// internal/media/upload/blossom/upload.go
package blossom

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/nbd-wtf/go-nostr"
	"git.sovbit.dev/Enki/nostr-poster/internal/scheduler"
	"git.sovbit.dev/Enki/nostr-poster/internal/utils"
	"go.uber.org/zap"
)

// BlobDescriptor represents metadata about a blob stored with Blossom
type BlobDescriptor struct {
	URL       string    `json:"url,omitempty"`
	SHA256    string    `json:"sha256,omitempty"`
	MimeType  string    `json:"mime_type,omitempty"`
	Size      int64     `json:"size,omitempty"`
	Height    int       `json:"height,omitempty"`
	Width     int       `json:"width,omitempty"`
	Alt       string    `json:"alt,omitempty"`
	Caption   string    `json:"caption,omitempty"`
	CreatedAt time.Time `json:"created_at,omitempty"`
}

// BlossomResponse represents the response from a Blossom upload
type BlossomResponse struct {
    Status         string  `json:"status"`
    Message        string  `json:"message"`
    URL            string  `json:"url"`
    SHA256         string  `json:"sha256"`
    Size           int64   `json:"size"`
    Type           string  `json:"type"`
    Uploaded       int64   `json:"uploaded"`
    BlurHash       string  `json:"blurhash"`
    Dim            string  `json:"dim"`
    PaymentRequest string  `json:"payment_request"`
    Visibility     int     `json:"visibility"`
}

// Uploader implements the media upload functionality for Blossom
type Uploader struct {
	serverURL     string
	logger        *zap.Logger
	// Function to get a signed auth header (for Blossom authentication)
	getAuthHeader func(url, method string) (string, error)
}

// NewUploader creates a new Blossom uploader
func NewUploader(
	serverURL string,
	logger *zap.Logger,
	getAuthHeader func(url, method string) (string, error),
) *Uploader {
	if logger == nil {
		// Create a default logger if none is provided
		var err error
		logger, err = zap.NewProduction()
		if err != nil {
			// If we can't create a logger, use a no-op logger
			logger = zap.NewNop()
		}
	}

	return &Uploader{
		serverURL:     serverURL,
		logger:        logger,
		getAuthHeader: getAuthHeader,
	}
}

// getEnhancedContentType tries multiple methods to get the most accurate content type
func getEnhancedContentType(filePath string) (string, error) {
    // Get extension to help with MIME type detection
    ext := strings.ToLower(filepath.Ext(filePath))
    
    // Try standard content type detection first
    contentType, err := utils.GetFileContentType(filePath)
    if err != nil {
        return "", fmt.Errorf("failed to determine content type: %w", err)
    }
    
    // Override with more specific MIME types for certain file extensions
    // These are common file extensions that sometimes get generic MIME types
    switch ext {
    case ".jpg", ".jpeg":
        return "image/jpeg", nil
    case ".png":
        return "image/png", nil
    case ".gif":
        return "image/gif", nil
    case ".webp":
        return "image/webp", nil
    case ".svg":
        return "image/svg+xml", nil
    case ".mp4":
        return "video/mp4", nil
    case ".webm":
        return "video/webm", nil
    case ".mov":
        return "video/quicktime", nil
    case ".mp3":
        return "audio/mpeg", nil
    case ".wav":
        return "audio/wav", nil
    case ".pdf":
        return "application/pdf", nil
    }
    
    // If we got a generic type like "application/octet-stream", try to be more specific
    if contentType == "application/octet-stream" || contentType == "text/plain" {
        // Add more specific mappings for binary files that might be detected as octet-stream
        switch ext {
        case ".jpg", ".jpeg":
            return "image/jpeg", nil
        case ".png":
            return "image/png", nil
        case ".mp4":
            return "video/mp4", nil
        }
    }
    
    return contentType, nil
}

// UploadFile uploads a file to a Blossom server using raw binary data in the request body
func (u *Uploader) UploadFile(filePath string, caption string, altText string) (string, string, error) {
    // Log information about the upload
    u.logger.Info("Uploading file to Blossom server",
        zap.String("filePath", filePath),
        zap.String("serverURL", u.serverURL))
        
    // Open the file
    file, err := os.Open(filePath)
    if err != nil {
        return "", "", fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()

    // Get file info
    fileInfo, err := file.Stat()
    if err != nil {
        return "", "", fmt.Errorf("failed to get file info: %w", err)
    }
    fileSize := fileInfo.Size()

    // Calculate file hash before reading the file for the request body
    hasher := sha256.New()
    if _, err := io.Copy(hasher, file); err != nil {
        return "", "", fmt.Errorf("failed to calculate file hash: %w", err)
    }
    fileHash := hex.EncodeToString(hasher.Sum(nil))

    // Reset file pointer to the beginning
    if _, err := file.Seek(0, 0); err != nil {
        return "", "", fmt.Errorf("failed to reset file: %w", err)
    }

    // Get enhanced content type
    contentType, err := getEnhancedContentType(filePath)
    if err != nil {
        return "", "", fmt.Errorf("failed to determine content type: %w", err)
    }
    
    u.logger.Info("File details",
        zap.String("filename", filepath.Base(filePath)),
        zap.String("contentType", contentType),
        zap.Int64("size", fileSize),
        zap.String("hash", fileHash))

    // Create the request with the file as the raw body
    // This follows BUD-02 which states the endpoint must accept binary data in the body
    req, err := http.NewRequest("PUT", u.serverURL, file)
    if err != nil {
        return "", "", fmt.Errorf("failed to create request: %w", err)
    }

    // Set headers
    req.Header.Set("Content-Type", contentType)
    req.Header.Set("Content-Length", fmt.Sprintf("%d", fileSize))
    
    // Add additional headers that might help the server
    req.Header.Set("X-Content-Type", contentType)
    req.Header.Set("X-File-Name", filepath.Base(filePath))
    req.Header.Set("X-File-Size", fmt.Sprintf("%d", fileSize))
    req.Header.Set("X-File-Hash", fileHash)
    
    // If we have caption or alt text, add them as headers
    if caption != "" {
        req.Header.Set("X-Caption", caption)
    }
    if altText != "" {
        req.Header.Set("X-Alt", altText) 
    }

    // Add authorization header if available
    if u.getAuthHeader != nil {
        authHeader, err := u.getAuthHeader(u.serverURL, "PUT")
        if err != nil {
            return "", "", fmt.Errorf("failed to create auth header: %w", err)
        }
        req.Header.Set("Authorization", authHeader)
    }

    // Create HTTP client with timeout
    client := &http.Client{
        Timeout: 2 * time.Minute,
    }

    // Send the request
    u.logger.Info("Sending raw binary request to Blossom server", 
        zap.String("contentType", contentType),
        zap.Int64("contentLength", fileSize))
        
    resp, err := client.Do(req)
    if err != nil {
        return "", "", fmt.Errorf("failed to send request: %w", err)
    }
    defer resp.Body.Close()

    // Read the response body for logging
    bodyBytes, _ := io.ReadAll(resp.Body)
    bodyStr := string(bodyBytes)
    
    // Log response details
    u.logger.Info("Received response from server", 
        zap.Int("statusCode", resp.StatusCode),
        zap.String("body", bodyStr))

    // Check response status - accept 200, 201, and 202 as success
    if resp.StatusCode != http.StatusOK && 
       resp.StatusCode != http.StatusCreated && 
       resp.StatusCode != http.StatusAccepted {
        return "", "", fmt.Errorf("server returned non-success status: %d, body: %s", resp.StatusCode, bodyStr)
    }

    // Parse response
    var blossomResp BlossomResponse
    if err := json.Unmarshal(bodyBytes, &blossomResp); err != nil {
        return "", "", fmt.Errorf("failed to parse response: %w", err)
    }

    // Check for success
    if blossomResp.Status != "success" {
        return "", "", fmt.Errorf("upload failed: %s", blossomResp.Message)
    }

    // Log the successful response
    u.logger.Info("Upload successful",
        zap.String("url", blossomResp.URL),
        zap.String("hash", blossomResp.SHA256),
        zap.String("dimensions", blossomResp.Dim),
        zap.Int64("size", blossomResp.Size))

    // Use the URL directly from the response
    return blossomResp.URL, blossomResp.SHA256, nil
}

// DeleteFile deletes a file from the Blossom server
func (u *Uploader) DeleteFile(fileHash string) error {
	// Create the delete URL
	deleteURL := fmt.Sprintf("%s/%s", u.serverURL, fileHash)

	// Create the request
	req, err := http.NewRequest("DELETE", deleteURL, nil)
	if err != nil {
		return fmt.Errorf("failed to create delete request: %w", err)
	}

	// Add authorization header if available
	if u.getAuthHeader != nil {
		authHeader, err := u.getAuthHeader(deleteURL, "DELETE")
		if err != nil {
			return fmt.Errorf("failed to create auth header: %w", err)
		}

		req.Header.Set("Authorization", authHeader)
	}

	// Send the request
	client := &http.Client{
		Timeout: 30 * time.Second,
	}

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to send delete request: %w", err)
	}
	defer resp.Body.Close()

	// Check response status
	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("server returned non-OK status for delete: %d, body: %s", resp.StatusCode, string(bodyBytes))
	}

	return nil
}

// CreateBlossomAuthHeader creates a Blossom authentication header
func CreateBlossomAuthHeader(fullURL, method string, privkey string) (string, error) {
	// Parse the URL to extract the path for the endpoint
	parsed, err := url.Parse(fullURL)
	var endpoint string
	if err == nil && parsed.Path != "" {
		// Use the full path as the endpoint
		endpoint = parsed.Path
		// Remove leading slash if present
		endpoint = strings.TrimPrefix(endpoint, "/")
	} else {
		// Fallback to the full URL if parsing fails
		endpoint = fullURL
	}

	// Set an expiration 5 minutes in the future
	expiration := time.Now().Add(5 * time.Minute).Unix()
	
	// Set the operation type for BUD-02 compliance
	operation := "upload"
	if method == "DELETE" {
		operation = "delete"
	} else if strings.Contains(endpoint, "list") {
		operation = "list"
	}
	
	// Create the tags array with required Blossom tags
	tags := []nostr.Tag{
		{"t", operation}, // Required tag for Blossom auth
		{"u", endpoint},  // URL endpoint
		{"method", method},
		{"expiration", fmt.Sprintf("%d", expiration)},
	}

	// Create the auth event
	authEvent := nostr.Event{
		Kind:      24242,
		CreatedAt: nostr.Timestamp(time.Now().Unix()),
		Tags:      tags,
		Content:   "",
	}

	// Get the public key from the private key
	pubkey, err := nostr.GetPublicKey(privkey)
	if err != nil {
		return "", fmt.Errorf("failed to get public key: %w", err)
	}
	authEvent.PubKey = pubkey

	// Sign the event
	if err := authEvent.Sign(privkey); err != nil {
		return "", fmt.Errorf("failed to sign auth event: %w", err)
	}

	// Serialize the event
	eventJSON, err := json.Marshal(authEvent)
	if err != nil {
		return "", fmt.Errorf("failed to serialize auth event: %w", err)
	}

	// Encode as base64
	encodedEvent := base64.StdEncoding.EncodeToString(eventJSON)

	// Return the authorization header
	return "Nostr " + encodedEvent, nil
}

// WithCustomURL creates a new uploader instance with the specified custom URL.
func (u *Uploader) WithCustomURL(customURL string) scheduler.MediaUploader {
	return &Uploader{
		serverURL:     customURL,
		logger:        u.logger,
		getAuthHeader: u.getAuthHeader,
	}
}