package runtimeattachments

import (
	"fmt"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/promptcompiler"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const (
	CacheDirMode  = 0700
	CacheFileMode = 0600
)

// CacheDir returns the attachment cache beneath configDir.
func CacheDir(configDir string) string {
	return filepath.Join(configDir, "attachments")
}

// FindCachedPath returns the first cached file for attachmentID.
func FindCachedPath(cacheDir string, attachmentID string) (string, bool) {
	entries, err := os.ReadDir(cacheDir)
	if err != nil {
		return "", false
	}
	base := safeCacheBase(attachmentID)
	for _, entry := range entries {
		name := entry.Name()
		if name == base || strings.HasPrefix(name, base+".") {
			return filepath.Join(cacheDir, name), true
		}
	}
	return "", false
}

// CacheFilename returns a path-safe cache filename for an attachment.
func CacheFilename(id string, filename string, contentType string) string {
	ext := ExtensionFromContentType(contentType)
	if ext == "" {
		ext = strings.ToLower(filepath.Ext(filename))
	}
	return safeCacheBase(id) + ext
}

func safeCacheBase(id string) string {
	id = strings.TrimSpace(id)
	if id == "" {
		return "attachment"
	}
	var b strings.Builder
	for _, r := range id {
		switch r {
		case '/', '\\', ':', 0:
			b.WriteByte('-')
		default:
			b.WriteRune(r)
		}
	}
	return b.String()
}

// FromPath builds the runtime-facing metadata for a cached attachment.
func FromPath(attachment types.MessageAttachment, localPath string, contentType string) types.RuntimeAttachment {
	info, _ := os.Stat(localPath)
	if strings.TrimSpace(contentType) == "" {
		contentType = strings.TrimSpace(attachment.ContentType)
	}
	if strings.TrimSpace(contentType) == "" {
		contentType = mime.TypeByExtension(strings.ToLower(filepath.Ext(localPath)))
	}
	if strings.TrimSpace(contentType) == "" {
		contentType = "application/octet-stream"
	}
	filename := strings.TrimSpace(attachment.Filename)
	if filename == "" {
		filename = filepath.Base(localPath)
	} else {
		filename = filepath.Base(filename)
	}
	var size int64
	if info != nil {
		size = info.Size()
	}
	if size <= 0 && attachment.SizeBytes > 0 {
		size = attachment.SizeBytes
	}
	return types.RuntimeAttachment{
		ID:       strings.TrimSpace(attachment.ID),
		Filename: filename,
		MIME:     contentType,
		Path:     localPath,
		URI:      FileURI(localPath),
		Size:     size,
	}
}

// NormalizeMIME resolves an attachment MIME type from headers, name, or data.
func NormalizeMIME(contentType string, filename string, data []byte) string {
	contentType = strings.TrimSpace(strings.Split(contentType, ";")[0])
	if contentType == "" || contentType == "application/octet-stream" {
		if byExt := mime.TypeByExtension(strings.ToLower(filepath.Ext(filename))); byExt != "" {
			return byExt
		}
		if len(data) > 0 {
			return http.DetectContentType(data)
		}
	}
	return contentType
}

// FileURI converts a local path to a file URI.
func FileURI(localPath string) string {
	return (&url.URL{Scheme: "file", Path: localPath}).String()
}

// RenderManifest returns a portable prompt block for runtimes that need local
// file paths instead of native file parts. It does not parse attachment content.
func RenderManifest(attachments []types.RuntimeAttachment) string {
	return promptcompiler.RenderAttachmentResolution(promptcompiler.AttachmentResolution{
		Prepared: normalizeRuntimeAttachments(attachments),
	})
}

// AppendManifest appends a prepared-attachment manifest to text.
func AppendManifest(text string, attachments []types.RuntimeAttachment) string {
	return promptcompiler.AppendAttachmentResolution(text, promptcompiler.AttachmentResolution{
		Prepared: normalizeRuntimeAttachments(attachments),
	})
}

// RenderPreparationFailure returns a prompt-visible attachment preparation
// failure. It still does not inline or parse file contents.
func RenderPreparationFailure(attachments []types.MessageAttachment, err error) string {
	return promptcompiler.RenderAttachmentResolution(promptcompiler.AttachmentResolutionError(attachments, err))
}

// AppendPreparationFailure appends a prompt-visible preparation failure to text.
func AppendPreparationFailure(text string, attachments []types.MessageAttachment, err error) string {
	return promptcompiler.AppendAttachmentResolution(text, promptcompiler.AttachmentResolutionError(attachments, err))
}

// LocalPath returns the local filesystem path for a runtime attachment when it
// can be derived from either Path or a file:// URI.
func LocalPath(attachment types.RuntimeAttachment) string {
	if path := strings.TrimSpace(attachment.Path); path != "" {
		return path
	}
	uri := strings.TrimSpace(attachment.URI)
	if uri == "" {
		return ""
	}
	parsed, err := url.Parse(uri)
	if err != nil || parsed.Scheme != "file" {
		return ""
	}
	return parsed.Path
}

func normalizeRuntimeAttachments(attachments []types.RuntimeAttachment) []types.RuntimeAttachment {
	if len(attachments) == 0 {
		return nil
	}
	out := make([]types.RuntimeAttachment, 0, len(attachments))
	for _, attachment := range attachments {
		if strings.TrimSpace(attachment.ID) == "" &&
			strings.TrimSpace(attachment.Filename) == "" &&
			strings.TrimSpace(attachment.URI) == "" &&
			strings.TrimSpace(attachment.Path) == "" {
			continue
		}
		out = append(out, attachment)
	}
	return out
}

func attachmentLabel(attachment types.RuntimeAttachment) string {
	if filename := strings.TrimSpace(attachment.Filename); filename != "" {
		return filename
	}
	if path := LocalPath(attachment); path != "" {
		return path
	}
	if id := strings.TrimSpace(attachment.ID); id != "" {
		return id
	}
	return "unnamed attachment"
}

// MetadataSummary returns portable attachment metadata for runtime prompts.
func MetadataSummary(filename string, contentType string, size int64) string {
	var parts []string
	if ext := extension(filename, contentType); ext != "" {
		parts = append(parts, "ext:"+ext)
	}
	if normalizedContentType := normalizedContentType(contentType); normalizedContentType != "" {
		parts = append(parts, "mime:"+normalizedContentType)
	}
	if size >= 0 {
		parts = append(parts, fmt.Sprintf("size:%d bytes", size))
	}
	return strings.Join(parts, ", ")
}

func extension(filename string, contentType string) string {
	if ext := strings.ToLower(filepath.Ext(filename)); ext != "" {
		return ext
	}
	return ExtensionFromContentType(contentType)
}

// ContentTypeForPath returns the MIME type inferred from a local path.
func ContentTypeForPath(path string) string {
	if contentType := mime.TypeByExtension(strings.ToLower(filepath.Ext(path))); strings.TrimSpace(contentType) != "" {
		return contentType
	}
	return "application/octet-stream"
}

// MimeFormDataFileDisposition formats a multipart file disposition.
func MimeFormDataFileDisposition(fieldName string, filename string) string {
	return mime.FormatMediaType("form-data", map[string]string{
		"name":     fieldName,
		"filename": filename,
	})
}

// ExtensionFromContentType returns the preferred extension for a MIME type.
func ExtensionFromContentType(contentType string) string {
	contentType = normalizedContentType(contentType)
	if contentType == "" {
		return ""
	}
	switch contentType {
	case "image/png":
		return ".png"
	case "image/jpeg":
		return ".jpg"
	case "image/gif":
		return ".gif"
	case "image/webp":
		return ".webp"
	case "image/svg+xml":
		return ".svg"
	case "application/pdf":
		return ".pdf"
	case "application/json":
		return ".json"
	case "text/csv":
		return ".csv"
	case "application/msword":
		return ".doc"
	case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
		return ".docx"
	case "application/vnd.ms-excel":
		return ".xls"
	case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
		return ".xlsx"
	case "application/vnd.ms-powerpoint":
		return ".ppt"
	case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
		return ".pptx"
	}
	if strings.HasPrefix(contentType, "text/") {
		return ".txt"
	}
	if extensions, err := mime.ExtensionsByType(contentType); err == nil && len(extensions) > 0 {
		return extensions[0]
	}
	return ""
}

func normalizedContentType(contentType string) string {
	return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
}
