package tools

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

	"chat-switchboard/storage"
	"chat-switchboard/store"
)

// ── Late Registration ────────────────────────
// file_recall needs stores + objStore, which aren't available at
// init time. Called from main.go after storage init.
// If objStore is nil (storage not configured), the tool is not registered.

func RegisterFileRecall(stores store.Stores, objStore storage.ObjectStore) {
	if objStore == nil {
		log.Printf("ℹ file_recall: storage not configured, tool not registered")
		return
	}
	Register(&fileRecallTool{
		stores:   stores,
		objStore: objStore,
	})
}

// ═══════════════════════════════════════════
// file_recall
// ═══════════════════════════════════════════

const maxImageBytes = 10 * 1024 * 1024 // 10 MB cap for base64 images

type fileRecallTool struct {
	BaseTool
	stores   store.Stores
	objStore storage.ObjectStore
}

func (t *fileRecallTool) Definition() ToolDef {
	return ToolDef{
		Name:        "file_recall",
		DisplayName: "Files",
		Category:    "context",
		Description: "Re-read files from this conversation. " +
			"Use action='list' to see available files, then action='read' " +
			"with a file_id to retrieve the file content. " +
			"Documents return extracted text; images return base64 data.",
		Parameters: JSONSchema(map[string]interface{}{
			"action":        PropEnum("Action: 'list' to see files, 'read' to get content", "list", "read"),
			"file_id": Prop("string", "File ID to read (required for action='read')"),
		}, []string{"action"}),
	}
}

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

	switch args.Action {
	case "list":
		return t.listFiles(ctx, execCtx)
	case "read":
		if args.FileID == "" {
			return "", fmt.Errorf("file_id is required for action='read'")
		}
		return t.readFile(ctx, execCtx, args.FileID)
	default:
		return "", fmt.Errorf("invalid action: %q (use 'list' or 'read')", args.Action)
	}
}

// listFiles returns metadata for all files in the channel.
func (t *fileRecallTool) listFiles(ctx context.Context, execCtx ExecutionContext) (string, error) {
	atts, err := t.stores.Files.GetByChannel(ctx, execCtx.ChannelID, "")
	if err != nil {
		return "", fmt.Errorf("failed to list files: %w", err)
	}

	type attInfo struct {
		ID          string `json:"id"`
		Filename    string `json:"filename"`
		ContentType string `json:"content_type"`
		SizeBytes   int64  `json:"size_bytes"`
		HasText     bool   `json:"has_text"`
		CreatedAt   string `json:"created_at"`
	}

	items := make([]attInfo, 0, len(atts))
	for _, a := range atts {
		items = append(items, attInfo{
			ID:          a.ID,
			Filename:    a.Filename,
			ContentType: a.ContentType,
			SizeBytes:   a.SizeBytes,
			HasText:     a.ExtractedText != nil && *a.ExtractedText != "",
			CreatedAt:   a.CreatedAt.Format("2006-01-02T15:04:05Z"),
		})
	}

	log.Printf("📎 file_recall: list → %d files in channel %s", len(items), execCtx.ChannelID)

	out, _ := json.Marshal(map[string]interface{}{
		"files": items,
		"count":       len(items),
		"channel_id":  execCtx.ChannelID,
	})
	return string(out), nil
}

// readFile returns the content of a specific file.
func (t *fileRecallTool) readFile(ctx context.Context, execCtx ExecutionContext, fileID string) (string, error) {
	att, err := t.stores.Files.GetByID(ctx, fileID)
	if err != nil {
		return "", fmt.Errorf("file not found: %s", fileID)
	}

	// Security: verify file belongs to this channel
	if att.ChannelID != execCtx.ChannelID {
		return "", fmt.Errorf("file %s does not belong to this conversation", fileID)
	}

	// Documents: return extracted text
	if !isImageType(att.ContentType) {
		if att.ExtractedText != nil && *att.ExtractedText != "" {
			log.Printf("📎 file_recall: read text → %s (%s, %d chars)",
				att.Filename, att.ID, len(*att.ExtractedText))
			out, _ := json.Marshal(map[string]interface{}{
				"id":                    att.ID,
				"filename":              att.Filename,
				"content_type":          att.ContentType,
				"content":               *att.ExtractedText,
				"content_type_returned": "text",
			})
			return string(out), nil
		}
		return "", fmt.Errorf("no extracted text available for %s (content type: %s)", att.Filename, att.ContentType)
	}

	// Images: read from storage and return base64
	if att.SizeBytes > maxImageBytes {
		return "", fmt.Errorf("image %s is too large (%d bytes, max %d). Re-upload a smaller version",
			att.Filename, att.SizeBytes, maxImageBytes)
	}

	reader, size, _, err := t.objStore.Get(ctx, att.StorageKey)
	if err != nil {
		return "", fmt.Errorf("failed to read file from storage: %w", err)
	}
	defer reader.Close()

	// Read and base64-encode
	data := make([]byte, size)
	if _, err := io.ReadFull(reader, data); err != nil {
		return "", fmt.Errorf("failed to read file data: %w", err)
	}

	b64 := base64.StdEncoding.EncodeToString(data)
	dataURI := fmt.Sprintf("data:%s;base64,%s", att.ContentType, b64)

	log.Printf("📎 file_recall: read image → %s (%s, %d bytes)",
		att.Filename, att.ID, size)

	out, _ := json.Marshal(map[string]interface{}{
		"id":                    att.ID,
		"filename":              att.Filename,
		"content_type":          att.ContentType,
		"content":               dataURI,
		"content_type_returned": "base64_image",
	})
	return string(out), nil
}

func isImageType(ct string) bool {
	return strings.HasPrefix(ct, "image/")
}
