// internal/media/upload/nip94/upload.go
package nip94

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"time"

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

// NIP96ServerConfig represents the configuration for a NIP-96 server
type NIP96ServerConfig struct {
	APIURL           string   `json:"api_url"`
	DownloadURL      string   `json:"download_url,omitempty"`
	DelegatedToURL   string   `json:"delegated_to_url,omitempty"`
	SupportedContentTypes []string `json:"content_types,omitempty"`
}

// NIP96UploadResponse represents the response from a NIP-96 server upload
type NIP96UploadResponse struct {
	Status      string `json:"status"`
	Message     string `json:"message"`
	ProcessingURL string `json:"processing_url,omitempty"`
	NIP94Event  struct {
		Tags      [][]string `json:"tags"`
		Content   string     `json:"content"`
	} `json:"nip94_event"`
}

// Uploader implements the media upload functionality for NIP-94/96
type Uploader struct {
	serverURL   string
	downloadURL string
	supportedTypes []string
	logger      *zap.Logger
	// Function to get a signed auth header (for NIP-98)
	getAuthHeader func(url, method string, payload []byte) (string, error)
}

// NewUploader creates a new NIP-94/96 uploader
func NewUploader(
	serverURL string,
	downloadURL string,
	supportedTypes []string,
	logger *zap.Logger,
	getAuthHeader func(url, method string, payload []byte) (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,
		downloadURL: downloadURL,
		supportedTypes: supportedTypes,
		logger:      logger,
		getAuthHeader: getAuthHeader,
	}
}

// DiscoverServer discovers a NIP-96 server's configuration
func DiscoverServer(serverURL string) (*NIP96ServerConfig, error) {
	// Make sure we have the base URL without path
	baseURL := serverURL
	
	// Fetch the well-known JSON file
	resp, err := http.Get(baseURL + "/.well-known/nostr/nip96.json")
	if err != nil {
		return nil, fmt.Errorf("failed to fetch server configuration: %w", err)
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("server returned non-OK status: %d", resp.StatusCode)
	}
	
	// Parse the response
	var config NIP96ServerConfig
	if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
		return nil, fmt.Errorf("failed to parse server configuration: %w", err)
	}
	
	// If the server delegates to another URL, follow the delegation
	if config.APIURL == "" && config.DownloadURL == "" {
		delegatedURL := config.DelegatedToURL
		if delegatedURL == "" {
			return nil, errors.New("server configuration missing both api_url and delegated_to_url")
		}
		
		return DiscoverServer(delegatedURL)
	}
	
	return &config, nil
}

// UploadFile uploads a file to a NIP-96 compatible server
func (u *Uploader) UploadFile(filePath string, caption string, altText string) (string, string, error) {
	// 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)
	}
	
	// Calculate file hash
	hasher := sha256.New()
	if _, err := io.Copy(hasher, file); err != nil {
		return "", "", fmt.Errorf("failed to calculate file hash: %w", err)
	}
	
	// Reset file pointer
	if _, err := file.Seek(0, 0); err != nil {
		return "", "", fmt.Errorf("failed to reset file: %w", err)
	}
	
	// Get hash as hex
	fileHash := hex.EncodeToString(hasher.Sum(nil))
	
	// Get content type
	contentType, err := utils.GetFileContentType(filePath)
	if err != nil {
		return "", "", fmt.Errorf("failed to determine content type: %w", err)
	}
	
	// Create a buffer for the multipart form
	var requestBody bytes.Buffer
	writer := multipart.NewWriter(&requestBody)
	
	// Add the file
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		return "", "", fmt.Errorf("failed to create form file: %w", err)
	}
	
	if _, err := io.Copy(part, file); err != nil {
		return "", "", fmt.Errorf("failed to copy file to form: %w", err)
	}
	
	// Add caption if provided
	if caption != "" {
		if err := writer.WriteField("caption", caption); err != nil {
			return "", "", fmt.Errorf("failed to add caption: %w", err)
		}
	}
	
	// Add alt text if provided
	if altText != "" {
		if err := writer.WriteField("alt", altText); err != nil {
			return "", "", fmt.Errorf("failed to add alt text: %w", err)
		}
	}
	
	// Add content type
	if err := writer.WriteField("content_type", contentType); err != nil {
		return "", "", fmt.Errorf("failed to add content type: %w", err)
	}
	
	// Add file size
	if err := writer.WriteField("size", fmt.Sprintf("%d", fileInfo.Size())); err != nil {
		return "", "", fmt.Errorf("failed to add file size: %w", err)
	}
	
	// Add file hash for integrity verification
	if err := writer.WriteField("hash", fileHash); err != nil {
		return "", "", fmt.Errorf("failed to add file hash: %w", err)
	}
	
	// Close the writer
	if err := writer.Close(); err != nil {
		return "", "", fmt.Errorf("failed to close multipart writer: %w", err)
	}
	
	// Create the request
	req, err := http.NewRequest("POST", u.serverURL, &requestBody)
	if err != nil {
		return "", "", fmt.Errorf("failed to create request: %w", err)
	}
	
	// Set content type
	req.Header.Set("Content-Type", writer.FormDataContentType())
	
	// Get the request body for the NIP-98 auth
	bodyBytes := requestBody.Bytes()

	// Add NIP-98 auth header if available
	if u.getAuthHeader != nil {
		// Calculate SHA-256 of the entire request body for more comprehensive authentication
		bodyHasher := sha256.New()
		bodyHasher.Write(bodyBytes)
		bodyHash := bodyHasher.Sum(nil)
		
		// Use the body hash for authentication
		authHeader, err := u.getAuthHeader(u.serverURL, "POST", bodyHash)
		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
	resp, err := client.Do(req)
	if err != nil {
		return "", "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()
	
	// Check response status
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return "", "", fmt.Errorf("server returned non-success status: %d, body: %s", resp.StatusCode, string(bodyBytes))
	}
	
	// Parse response
	var uploadResp NIP96UploadResponse
	if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
		return "", "", fmt.Errorf("failed to parse response: %w", err)
	}
	
	// Check if we need to wait for processing
	if uploadResp.Status == "processing" && uploadResp.ProcessingURL != "" {
		// Wait for processing to complete
		return u.waitForProcessing(uploadResp.ProcessingURL)
	}
	
	// Check for success
	if uploadResp.Status != "success" {
		return "", "", fmt.Errorf("upload failed: %s", uploadResp.Message)
	}
	
	// Extract URL and hash from the NIP-94 event
	var mediaURL string
	var mediaHash string
	
	for _, tag := range uploadResp.NIP94Event.Tags {
		if len(tag) >= 2 {
			if tag[0] == "url" {
				mediaURL = tag[1]
			} else if tag[0] == "ox" || tag[0] == "x" {
				mediaHash = tag[1]
			}
		}
	}
	
	if mediaURL == "" {
		return "", "", errors.New("missing URL in response")
	}
	
	// Verify the hash matches what we calculated
	if mediaHash != "" && mediaHash != fileHash {
		u.logger.Warn("Server returned different hash than calculated",
			zap.String("calculated", fileHash),
			zap.String("returned", mediaHash))
	}
	
	return mediaURL, mediaHash, nil
}

// waitForProcessing waits for a file processing to complete
func (u *Uploader) waitForProcessing(processingURL string) (string, string, error) {
	// Create HTTP client
	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	
	// Try several times
	maxAttempts := 10
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		// Wait before retry
		if attempt > 1 {
			time.Sleep(time.Duration(attempt) * time.Second)
		}
		
		// Check processing status
		resp, err := client.Get(processingURL)
		if err != nil {
			u.logger.Warn("Failed to check processing status",
				zap.String("url", processingURL),
				zap.Error(err),
				zap.Int("attempt", attempt))
			continue
		}
		
		// Read response body
		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			u.logger.Warn("Failed to read processing response",
				zap.Error(err),
				zap.Int("attempt", attempt))
			continue
		}
		
		// Check if processing is complete
		if resp.StatusCode == http.StatusCreated {
			// Parse the complete response
			var uploadResp NIP96UploadResponse
			if err := json.Unmarshal(body, &uploadResp); err != nil {
				return "", "", fmt.Errorf("failed to parse processing completion response: %w", err)
			}
			
			// Extract URL and hash from the NIP-94 event
			var mediaURL string
			var mediaHash string
			
			for _, tag := range uploadResp.NIP94Event.Tags {
				if len(tag) >= 2 {
					if tag[0] == "url" {
						mediaURL = tag[1]
					} else if tag[0] == "ox" || tag[0] == "x" {
						mediaHash = tag[1]
					}
				}
			}
			
			if mediaURL == "" {
				return "", "", errors.New("missing URL in processing completion response")
			}
			
			return mediaURL, mediaHash, nil
		}
		
		// Parse the processing status
		var statusResp struct {
			Status     string `json:"status"`
			Message    string `json:"message"`
			Percentage int    `json:"percentage"`
		}
		
		if err := json.Unmarshal(body, &statusResp); err != nil {
			u.logger.Warn("Failed to parse processing status response",
				zap.Error(err),
				zap.String("body", string(body)),
				zap.Int("attempt", attempt))
			continue
		}
		
		// Check if processing failed
		if statusResp.Status == "error" {
			return "", "", fmt.Errorf("processing failed: %s", statusResp.Message)
		}
		
		// Log progress
		u.logger.Info("File processing in progress",
			zap.String("url", processingURL),
			zap.Int("percentage", statusResp.Percentage),
			zap.Int("attempt", attempt))
	}
	
	return "", "", errors.New("processing timed out")
}

// DeleteFile deletes a file from the 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 NIP-98 auth header if available
	if u.getAuthHeader != nil {
		authHeader, err := u.getAuthHeader(deleteURL, "DELETE", nil)
		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
}

// CreateNIP98AuthHeader creates a NIP-98 authorization header
func CreateNIP98AuthHeader(url, method string, payload []byte, privkey string) (string, error) {
	// Create the event
	tags := []nostr.Tag{
		{"u", url},
		{"method", method},
	}
	
	// Add payload hash if provided
	if payload != nil {
		payloadHasher := sha256.New()
		payloadHasher.Write(payload)
		payloadHash := hex.EncodeToString(payloadHasher.Sum(nil))
		tags = append(tags, nostr.Tag{"payload", payloadHash})
	}
	
	// Create the auth event
	authEvent := nostr.Event{
		Kind:      27235, // NIP-98 HTTP Auth
		CreatedAt: nostr.Timestamp(time.Now().Unix()),
		Tags:      tags,
		Content:   "", // Empty content for auth events
	}
	
	// Get the public 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
	err = authEvent.Sign(privkey)
	if 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 {
    // Create a new uploader with the same configuration but a different URL
    return &Uploader{
        serverURL:      customURL,
        downloadURL:    u.downloadURL,
        supportedTypes: u.supportedTypes,
        logger:         u.logger,
        getAuthHeader:  u.getAuthHeader,
    }
}