package handlers

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"io"
	"mime"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

	"git.elijahkuntz.com/Elijah/drive/config"
	"git.elijahkuntz.com/Elijah/drive/database"
	"git.elijahkuntz.com/Elijah/drive/workers"
	"github.com/gofiber/fiber/v2"
	"github.com/cespare/xxhash/v2"
)

// TusHandler implements a simplified tus v1.0.0 protocol for resumable uploads.
// This handles the core Creation and Termination extensions needed for reliable
// large file uploads over unstable connections.
type TusHandler struct {
	DB     *database.DB
	Config *config.Config
	mu     sync.RWMutex
	uploads map[string]*tusUpload
}

type tusUpload struct {
	ID       string
	FilePath string // final destination path (relative)
	FileName string
	FileSize int64
	Offset   int64
	TempPath string // path to the incomplete upload on disk
	MimeType string
}

func NewTusHandler(db *database.DB, cfg *config.Config) *TusHandler {
	return &TusHandler{
		DB:      db,
		Config:  cfg,
		uploads: make(map[string]*tusUpload),
	}
}

// Options returns tus protocol capability headers.
// OPTIONS /api/tus
func (h *TusHandler) Options(c *fiber.Ctx) error {
	c.Set("Tus-Resumable", "1.0.0")
	c.Set("Tus-Version", "1.0.0")
	c.Set("Tus-Extension", "creation,termination")
	c.Set("Tus-Max-Size", "107374182400") // 100 GB
	return c.SendStatus(fiber.StatusNoContent)
}

// Create initializes a new resumable upload.
// POST /api/tus
func (h *TusHandler) Create(c *fiber.Ctx) error {
	uploadLength := c.Get("Upload-Length")
	if uploadLength == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Upload-Length header is required"})
	}

	fileSize, err := strconv.ParseInt(uploadLength, 10, 64)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid Upload-Length"})
	}

	// Parse metadata (contains filename and destination path)
	metadata := parseTusMetadata(c.Get("Upload-Metadata"))
	fileName := metadata["filename"]
	destPath := metadata["path"]

	if fileName == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "filename metadata is required"})
	}
	if destPath == "" {
		destPath = "."
	}

	// Generate unique upload ID
	uploadID, err := generateUploadID()
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate upload ID"})
	}

	// Create temp file for the incomplete upload
	tempDir := filepath.Join(h.Config.StorageDir, ".uploads")
	os.MkdirAll(tempDir, 0755)
	tempPath := filepath.Join(tempDir, uploadID)

	// Create the empty temp file
	f, err := os.Create(tempPath)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create upload"})
	}
	f.Close()

	upload := &tusUpload{
		ID:       uploadID,
		FilePath: filepath.ToSlash(filepath.Join(destPath, fileName)),
		FileName: fileName,
		FileSize: fileSize,
		Offset:   0,
		TempPath: tempPath,
		MimeType: mime.TypeByExtension(filepath.Ext(fileName)),
	}

	h.mu.Lock()
	h.uploads[uploadID] = upload
	h.mu.Unlock()

	c.Set("Tus-Resumable", "1.0.0")
	c.Set("Location", fmt.Sprintf("/api/tus/%s", uploadID))
	c.Set("Upload-Offset", "0")

	return c.SendStatus(fiber.StatusCreated)
}

// Head returns the current offset of an upload (for resuming).
// HEAD /api/tus/:id
func (h *TusHandler) Head(c *fiber.Ctx) error {
	uploadID := c.Params("id")

	h.mu.RLock()
	upload, exists := h.uploads[uploadID]
	h.mu.RUnlock()

	if !exists {
		return c.SendStatus(fiber.StatusNotFound)
	}

	c.Set("Tus-Resumable", "1.0.0")
	c.Set("Upload-Offset", strconv.FormatInt(upload.Offset, 10))
	c.Set("Upload-Length", strconv.FormatInt(upload.FileSize, 10))

	return c.SendStatus(fiber.StatusOK)
}

// Patch appends a chunk of data to an existing upload.
// PATCH /api/tus/:id
func (h *TusHandler) Patch(c *fiber.Ctx) error {
	uploadID := c.Params("id")

	h.mu.RLock()
	upload, exists := h.uploads[uploadID]
	h.mu.RUnlock()

	if !exists {
		return c.SendStatus(fiber.StatusNotFound)
	}

	// Validate offset matches
	clientOffset := c.Get("Upload-Offset")
	if clientOffset == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Upload-Offset header required"})
	}

	offset, err := strconv.ParseInt(clientOffset, 10, 64)
	if err != nil || offset != upload.Offset {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{
			"error":           "offset mismatch",
			"expected_offset": upload.Offset,
		})
	}

	// Open temp file at the correct offset and write the chunk
	f, err := os.OpenFile(upload.TempPath, os.O_WRONLY, 0644)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to open upload file"})
	}
	defer f.Close()

	if _, err := f.Seek(upload.Offset, io.SeekStart); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "seek failed"})
	}

	body := c.Body()
	n, err := f.Write(body)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "write failed"})
	}

	// Update offset
	h.mu.Lock()
	upload.Offset += int64(n)
	h.mu.Unlock()

	c.Set("Tus-Resumable", "1.0.0")
	c.Set("Upload-Offset", strconv.FormatInt(upload.Offset, 10))

	// Check if upload is complete
	if upload.Offset >= upload.FileSize {
		h.finalizeUpload(upload, c.IP())
	}

	return c.SendStatus(fiber.StatusNoContent)
}

// Delete cancels an in-progress upload (Termination extension).
// DELETE /api/tus/:id
func (h *TusHandler) Delete(c *fiber.Ctx) error {
	uploadID := c.Params("id")

	h.mu.Lock()
	upload, exists := h.uploads[uploadID]
	if exists {
		delete(h.uploads, uploadID)
	}
	h.mu.Unlock()

	if !exists {
		return c.SendStatus(fiber.StatusNotFound)
	}

	os.Remove(upload.TempPath)

	return c.SendStatus(fiber.StatusNoContent)
}

// finalizeUpload moves the completed temp file to its destination and computes checksum.
func (h *TusHandler) finalizeUpload(upload *tusUpload, ip string) {
	// Resolve final destination
	destFull := filepath.Join(h.Config.StorageDir, filepath.FromSlash(upload.FilePath))

	// Ensure parent dir exists
	os.MkdirAll(filepath.Dir(destFull), 0755)

	// Version existing file if it exists
	CreateVersion(h.DB, h.Config, destFull, filepath.ToSlash(upload.FilePath))

	// Move temp file to final location
	if err := os.Rename(upload.TempPath, destFull); err != nil {
		fmt.Printf("Error finalizing upload %s: %v\n", upload.ID, err)
		return
	}

	// Generate XXHash checksum
	checksum := ""
	if f, err := os.Open(destFull); err == nil {
		hasher := xxhash.New()
		io.Copy(hasher, f)
		f.Close()
		checksum = fmt.Sprintf("%016x", hasher.Sum64())
	}

	// Upsert file record
	h.DB.Exec(`
		INSERT INTO files (path, name, size, mime_type, checksum)
		VALUES (?, ?, ?, ?, ?)
		ON CONFLICT(path) DO UPDATE SET
			size = excluded.size,
			mime_type = excluded.mime_type,
			checksum = excluded.checksum,
			updated_at = CURRENT_TIMESTAMP
	`, upload.FilePath, upload.FileName, upload.FileSize, upload.MimeType, checksum)

	h.DB.AddAuditLog("upload_complete", fmt.Sprintf("%s (%d bytes, tus)", upload.FilePath, upload.FileSize), ip)

	// Queue thumbnail generation
	if workers.Instance != nil {
		workers.Instance.Enqueue(workers.ThumbnailJob{
			FilePath: upload.FilePath,
			Checksum: checksum,
			MimeType: upload.MimeType,
		})
	}

	// Remove from active uploads
	h.mu.Lock()
	delete(h.uploads, upload.ID)
	h.mu.Unlock()

	fmt.Printf("✅ Upload finalized: %s (checksum: %s)\n", upload.FilePath, checksum)
}

// --- Helpers ---

func parseTusMetadata(header string) map[string]string {
	result := make(map[string]string)
	if header == "" {
		return result
	}

	pairs := strings.Split(header, ",")
	for _, pair := range pairs {
		pair = strings.TrimSpace(pair)
		parts := strings.SplitN(pair, " ", 2)
		if len(parts) == 2 {
			// tus metadata values are base64 encoded
			decoded, err := decodeBase64(parts[1])
			if err == nil {
				result[parts[0]] = decoded
			}
		} else if len(parts) == 1 {
			result[parts[0]] = ""
		}
	}
	return result
}

func decodeBase64(s string) (string, error) {
	// Try standard base64 first, then URL-safe
	decoded, err := base64.StdEncoding.DecodeString(s)
	if err != nil {
		decoded, err = base64.RawStdEncoding.DecodeString(s)
	}
	return string(decoded), err
}

func generateUploadID() (string, error) {
	bytes := make([]byte, 16)
	if _, err := rand.Read(bytes); err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}
