package app

import (
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/lukaszraczylo/gohoarder/internal/version"
	"github.com/lukaszraczylo/gohoarder/pkg/metadata"
	"github.com/lukaszraczylo/gohoarder/pkg/websocket"
	"github.com/rs/zerolog/log"
)

// handlePackages handles /api/packages endpoint
func (a *App) handlePackages(c *fiber.Ctx) error {
	c.Set("Content-Type", "application/json")
	c.Set("Access-Control-Allow-Origin", "*")
	c.Set("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS")
	c.Set("Access-Control-Allow-Headers", "Content-Type")

	if c.Method() == "OPTIONS" {
		return c.SendStatus(fiber.StatusOK)
	}

	// Check if this is a vulnerability endpoint request
	if strings.HasSuffix(c.Path(), "/vulnerabilities") {
		return a.handleVulnerabilities(c)
	}

	switch c.Method() {
	case "GET":
		return a.handleListPackages(c)
	case "DELETE":
		return a.handleDeletePackage(c)
	default:
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
	}
}

// handleListPackages returns list of cached packages
func (a *App) handleListPackages(c *fiber.Ctx) error {
	ctx := c.Context()

	// Get packages from metadata store
	allPackages, err := a.metadata.ListPackages(ctx, &metadata.ListOptions{
		Limit:  1000, // Get more to account for duplicates
		Offset: 0,
	})
	if err != nil {
		log.Error().Err(err).Msg("Failed to list packages")
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list packages"})
	}

	log.Debug().Int("total_packages_from_db", len(allPackages)).Msg("Retrieved packages from database")

	// Filter, clean, and deduplicate packages
	// Map stores both cleaned package and original name for scan lookups
	type packageEntry struct {
		pkg          *metadata.Package
		originalName string
	}
	seen := make(map[string]*packageEntry)
	skippedCount := 0
	for _, pkg := range allPackages {
		// Skip metadata entries (npm metadata pages, pypi pages, etc.)
		if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
			skippedCount++
			log.Debug().
				Str("name", pkg.Name).
				Str("version", pkg.Version).
				Str("registry", pkg.Registry).
				Msg("Skipping metadata entry")
			continue
		}

		// Clean the package name (remove /@v/version.ext suffix)
		originalName := pkg.Name
		cleanName := pkg.Name
		if idx := strings.Index(cleanName, "/@v/"); idx != -1 {
			cleanName = cleanName[:idx]
		}

		// Create deduplication key
		key := cleanName + "@" + pkg.Version

		// Keep the entry with the largest size (typically .zip files)
		if existing, ok := seen[key]; !ok || pkg.Size > existing.pkg.Size {
			// Create a copy with cleaned name
			cleanPkg := *pkg
			cleanPkg.Name = cleanName
			seen[key] = &packageEntry{
				pkg:          &cleanPkg,
				originalName: originalName,
			}
		}
	}

	log.Debug().
		Int("skipped_metadata", skippedCount).
		Int("unique_packages", len(seen)).
		Msg("Filtered and deduplicated packages")

	// Convert map to slice, keeping track of original names
	type packageWithOriginalName struct {
		pkg          *metadata.Package
		originalName string
	}
	packagesWithNames := make([]packageWithOriginalName, 0, len(seen))
	for _, entry := range seen {
		packagesWithNames = append(packagesWithNames, packageWithOriginalName{
			pkg:          entry.pkg,
			originalName: entry.originalName,
		})
	}

	// Enhance packages with vulnerability information if security scanning is enabled
	var response map[string]interface{}
	if a.config.Security.Enabled {
		enhancedPackages := make([]map[string]interface{}, 0, len(packagesWithNames))
		for _, entry := range packagesWithNames {
			pkg := entry.pkg
			pkgMap := map[string]interface{}{
				"id":              pkg.ID,
				"registry":        pkg.Registry,
				"name":            pkg.Name,
				"version":         pkg.Version,
				"size":            pkg.Size,
				"checksum_sha256": pkg.ChecksumSHA256,
				"cached_at":       pkg.CachedAt,
				"last_accessed":   pkg.LastAccessed,
				"download_count":  pkg.DownloadCount,
			}

			// Add vulnerability info if scanned
			if pkg.SecurityScanned {
				// Use original name for scan result lookup (handles Go packages with /@v/ suffix)
				scanResult, err := a.metadata.GetScanResult(ctx, pkg.Registry, entry.originalName, pkg.Version)
				if err == nil && scanResult != nil {
					// Count vulnerabilities by severity
					severityCounts := make(map[string]int)
					for _, vuln := range scanResult.Vulnerabilities {
						severityCounts[strings.ToUpper(vuln.Severity)]++
					}

					// Check if package should be blocked based on thresholds
					isBlocked := false
					if a.scanManager != nil {
						blocked, _, _ := a.scanManager.CheckVulnerabilities(ctx, pkg.Registry, entry.originalName, pkg.Version)
						isBlocked = blocked
					}

					pkgMap["vulnerabilities"] = map[string]interface{}{
						"scanned":   true,
						"status":    scanResult.Status,
						"scannedAt": scanResult.ScannedAt.Format(time.RFC3339),
						"counts": map[string]int{
							"critical": severityCounts["CRITICAL"],
							"high":     severityCounts["HIGH"],
							"moderate": severityCounts["MODERATE"],
							"low":      severityCounts["LOW"],
						},
						"total":     scanResult.VulnerabilityCount,
						"isBlocked": isBlocked,
					}
				} else {
					pkgMap["vulnerabilities"] = map[string]interface{}{
						"scanned":   false,
						"status":    "pending",
						"isBlocked": false,
					}
				}
			} else {
				pkgMap["vulnerabilities"] = map[string]interface{}{
					"scanned":   false,
					"status":    "not_scanned",
					"isBlocked": false,
				}
			}

			enhancedPackages = append(enhancedPackages, pkgMap)
		}

		response = map[string]interface{}{
			"packages": enhancedPackages,
			"total":    len(enhancedPackages),
		}
	} else {
		// Non-enhanced mode - just return the packages
		packages := make([]*metadata.Package, 0, len(packagesWithNames))
		for _, entry := range packagesWithNames {
			packages = append(packages, entry.pkg)
		}
		response = map[string]interface{}{
			"packages": packages,
			"total":    len(packages),
		}
	}

	// Success response
	return c.Status(fiber.StatusOK).JSON(response)
}

// handleDeletePackage deletes a cached package
func (a *App) handleDeletePackage(c *fiber.Ctx) error {
	ctx := c.Context()

	// Parse path: /api/packages/{registry}/{name}/{version}
	// For Go packages, name can contain slashes (e.g., github.com/user/repo)
	// Version is always the last segment
	path := strings.TrimPrefix(c.Path(), "/api/packages/")
	parts := strings.Split(path, "/")
	if len(parts) < 3 {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "invalid path format, expected /api/packages/{registry}/{name}/{version}",
		})
	}

	registry := parts[0]
	version := parts[len(parts)-1]
	name := strings.Join(parts[1:len(parts)-1], "/")

	// For Go packages, we need to find and delete all cache entries (.info, .mod, .zip)
	// For other registries, we can delete directly
	var deletedCount int
	var lastErr error

	if registry == "go" {
		// List all packages matching the base name and version
		allPackages, err := a.metadata.ListPackages(ctx, &metadata.ListOptions{
			Limit: 1000,
		})
		if err != nil {
			log.Error().Err(err).Msg("Failed to list packages for deletion")
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list packages"})
		}

		log.Debug().
			Str("registry", registry).
			Str("name", name).
			Str("version", version).
			Int("total_packages", len(allPackages)).
			Msg("Searching for packages to delete")

		// Find and delete all entries for this package
		for _, pkg := range allPackages {
			if pkg.Registry != registry || pkg.Version != version {
				continue
			}

			// Check if this package name matches (either exact or with /@v/ suffix)
			cleanName := pkg.Name
			if idx := strings.Index(cleanName, "/@v/"); idx != -1 {
				cleanName = cleanName[:idx]
			}

			log.Debug().
				Str("db_name", pkg.Name).
				Str("clean_name", cleanName).
				Str("search_name", name).
				Bool("matches", cleanName == name).
				Msg("Checking package")

			if cleanName == name {
				if err := a.cache.Delete(ctx, pkg.Registry, pkg.Name, pkg.Version); err != nil {
					log.Warn().
						Err(err).
						Str("registry", pkg.Registry).
						Str("name", pkg.Name).
						Str("version", pkg.Version).
						Msg("Failed to delete package variant")
					lastErr = err
				} else {
					deletedCount++
					log.Info().
						Str("registry", pkg.Registry).
						Str("name", pkg.Name).
						Str("version", pkg.Version).
						Msg("Deleted package variant")
				}
			}
		}

		log.Debug().
			Int("deleted_count", deletedCount).
			Msg("Delete operation completed")

		if deletedCount == 0 {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "package not found"})
		}

		if lastErr != nil && deletedCount == 0 {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete package"})
		}
	} else {
		// For NPM and PyPI, delete directly
		if err := a.cache.Delete(ctx, registry, name, version); err != nil {
			log.Error().
				Err(err).
				Str("registry", registry).
				Str("name", name).
				Str("version", version).
				Msg("Failed to delete package")
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete package"})
		}
		deletedCount = 1
	}

	// Broadcast event via WebSocket
	a.wsServer.Broadcast(websocket.EventPackageDeleted, map[string]interface{}{
		"registry": registry,
		"name":     name,
		"version":  version,
	})

	// Success response
	response := map[string]interface{}{
		"deleted": true,
		"package": map[string]string{
			"registry": registry,
			"name":     name,
			"version":  version,
		},
	}

	// For Go packages, include count of deleted variants
	if registry == "go" {
		response["deleted_count"] = deletedCount
	}

	return c.Status(fiber.StatusOK).JSON(response)
}

// handleStats handles /api/stats endpoint
func (a *App) handleStats(c *fiber.Ctx) error {
	c.Set("Content-Type", "application/json")
	c.Set("Access-Control-Allow-Origin", "*")
	c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
	c.Set("Access-Control-Allow-Headers", "Content-Type")

	if c.Method() == "OPTIONS" {
		return c.SendStatus(fiber.StatusOK)
	}

	if c.Method() != "GET" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
	}

	ctx := c.Context()

	// Get cache statistics for all registries from database
	cacheStats, err := a.cache.GetStats(ctx, "")
	if err != nil {
		log.Error().Err(err).Msg("Failed to get cache stats")
		cacheStats = &metadata.Stats{}
	}

	// Get all packages to calculate per-registry breakdown
	packages, err := a.metadata.ListPackages(ctx, nil)
	if err != nil {
		log.Error().Err(err).Msg("Failed to list packages")
		packages = []*metadata.Package{}
	}

	// Calculate per-registry breakdown and blocked packages count
	registryStats := make(map[string]map[string]interface{})
	blockedCount := int64(0)

	for _, pkg := range packages {
		// Skip metadata entries (npm metadata pages, pypi pages, etc.)
		if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
			continue
		}

		// Track per-registry stats
		if _, ok := registryStats[pkg.Registry]; !ok {
			registryStats[pkg.Registry] = map[string]interface{}{
				"count":     0,
				"size":      int64(0),
				"downloads": int64(0),
			}
		}
		registryStats[pkg.Registry]["count"] = registryStats[pkg.Registry]["count"].(int) + 1
		registryStats[pkg.Registry]["size"] = registryStats[pkg.Registry]["size"].(int64) + pkg.Size
		registryStats[pkg.Registry]["downloads"] = registryStats[pkg.Registry]["downloads"].(int64) + int64(pkg.DownloadCount)

		// Check if package is blocked (only if security scanning is enabled and package is scanned)
		if a.config.Security.Enabled && a.scanManager != nil && pkg.SecurityScanned {
			blocked, _, _ := a.scanManager.CheckVulnerabilities(ctx, pkg.Registry, pkg.Name, pkg.Version)
			if blocked {
				blockedCount++
			}
		}
	}

	// Combine statistics using database stats for accuracy
	stats := map[string]interface{}{
		"total_packages":      cacheStats.TotalPackages,
		"total_downloads":     cacheStats.TotalDownloads,
		"total_size":          cacheStats.TotalSize,
		"max_cache_size":      a.config.Cache.MaxSizeBytes,
		"cache_hits":          cacheStats.TotalDownloads,
		"cache_misses":        0, // TODO: Track cache misses
		"cache_evictions":     0, // TODO: Track evictions
		"cache_size":          cacheStats.TotalSize,
		"scanned_packages":    cacheStats.ScannedPackages,
		"vulnerable_packages": cacheStats.VulnerablePackages,
		"blocked_packages":    blockedCount,
	}

	// Convert registry stats to interface map
	registries := make(map[string]interface{})
	for registry, regStats := range registryStats {
		registries[registry] = regStats
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"stats":      stats,
		"registries": registries,
	})
}

// handleTimeSeriesStats handles /api/stats/timeseries endpoint
// Returns time-series download statistics for charts
func (a *App) handleTimeSeriesStats(c *fiber.Ctx) error {
	c.Set("Content-Type", "application/json")
	c.Set("Access-Control-Allow-Origin", "*")
	c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
	c.Set("Access-Control-Allow-Headers", "Content-Type")

	if c.Method() == "OPTIONS" {
		return c.SendStatus(fiber.StatusOK)
	}

	if c.Method() != "GET" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
	}

	ctx := c.Context()

	// Get query parameters
	period := c.Query("period", "1day") // Default to 1 day
	registry := c.Query("registry")     // Optional registry filter

	// Validate period
	validPeriods := map[string]bool{"1h": true, "1day": true, "7day": true, "30day": true}
	if !validPeriods[period] {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "invalid period, must be one of: 1h, 1day, 7day, 30day",
		})
	}

	// Get time-series stats
	stats, err := a.metadata.GetTimeSeriesStats(ctx, period, registry)
	if err != nil {
		log.Error().Err(err).Str("period", period).Str("registry", registry).Msg("Failed to get time-series stats")
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "failed to get time-series statistics",
		})
	}

	return c.Status(fiber.StatusOK).JSON(stats)
}

// handleConfig handles /api/config endpoint
// Returns runtime configuration for the frontend
func (a *App) handleConfig(c *fiber.Ctx) error {
	c.Set("Content-Type", "application/json")
	c.Set("Access-Control-Allow-Origin", "*")
	c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
	c.Set("Access-Control-Allow-Headers", "Content-Type")

	if c.Method() == "OPTIONS" {
		return c.SendStatus(fiber.StatusOK)
	}

	if c.Method() != "GET" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
	}

	// Build server URL from request
	scheme := "http"
	if c.Protocol() == "https" {
		scheme = "https"
	}
	serverURL := scheme + "://" + c.Hostname()

	config := map[string]interface{}{
		"server_url": serverURL,
		"version":    version.Version,
		"features": map[string]bool{
			"security_scanning": a.config.Security.Enabled,
			"websockets":        true,
		},
	}

	return c.Status(fiber.StatusOK).JSON(config)
}

// handleInfo handles /api/info endpoint
func (a *App) handleInfo(c *fiber.Ctx) error {
	c.Set("Content-Type", "application/json")
	c.Set("Access-Control-Allow-Origin", "*")
	c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
	c.Set("Access-Control-Allow-Headers", "Content-Type")

	if c.Method() == "OPTIONS" {
		return c.SendStatus(fiber.StatusOK)
	}

	if c.Method() != "GET" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
	}

	info := map[string]interface{}{
		"name":    "GoHoarder",
		"version": version.Version,
		"config": map[string]interface{}{
			"storage_backend":  a.config.Storage.Backend,
			"metadata_backend": a.config.Metadata.Backend,
			"cache_ttl":        a.config.Cache.DefaultTTL.String(),
			"max_cache_size":   a.config.Cache.MaxSizeBytes,
		},
		"features": map[string]bool{
			"security_scanning": a.config.Security.Enabled,
			"pre_warming":       a.prewarmWorker != nil,
			"websockets":        true,
			"analytics":         true,
		},
	}

	return c.Status(fiber.StatusOK).JSON(info)
}
