package engine

import (
	"context"
	"fmt"
	"strconv"
	"sync"

	lru "github.com/hashicorp/golang-lru/v2"
	"gitea.dockr.co/kev/go-jdenticon/internal/constants"
	"gitea.dockr.co/kev/go-jdenticon/internal/util"
	"golang.org/x/sync/singleflight"
)

// Hash position constants for extracting values from the hash string
const (
	// Shape type selection positions
	hashPosSideShape   = 2 // Position for side shape selection
	hashPosCornerShape = 4 // Position for corner shape selection
	hashPosCenterShape = 1 // Position for center shape selection

	// Rotation positions
	hashPosSideRotation   = 3  // Position for side shape rotation
	hashPosCornerRotation = 5  // Position for corner shape rotation
	hashPosCenterRotation = -1 // Center shapes use incremental rotation (no hash position)

	// Color selection positions
	hashPosColorStart = 8 // Starting position for color selection (8, 9, 10)

	// Hue extraction
	hashPosHueStart  = -7        // Start position for hue extraction (last 7 chars)
	hashPosHueLength = 7         // Number of characters for hue
	hueMaxValue      = 0xfffffff // Maximum hue value for normalization
)

// Grid and layout constants
const (
	gridSize        = 4 // Standard 4x4 grid for jdenticon layout
	paddingMultiple = 2 // Padding is applied on both sides (2x)
)

// Color conflict resolution constants
const (
	colorDarkGray    = 0 // Index for dark gray color
	colorDarkMain    = 4 // Index for dark main color
	colorLightGray   = 2 // Index for light gray color
	colorLightMain   = 3 // Index for light main color
	colorMidFallback = 1 // Fallback color index for conflicts
)

// Shape rendering constants
const (
	shapeColorIndexSides   = 0 // Color index for side shapes
	shapeColorIndexCorners = 1 // Color index for corner shapes
	shapeColorIndexCenter  = 2 // Color index for center shapes

	numColorSelections = 3 // Total number of color selections needed
)

// Icon represents a generated jdenticon with its configuration and geometry
type Icon struct {
	Hash   string
	Size   float64
	Config ColorConfig
	Shapes []ShapeGroup
}

// ShapeGroup represents a group of shapes with the same color
type ShapeGroup struct {
	Color     Color
	Shapes    []Shape
	ShapeType string
}

// Shape represents a single geometric shape. It acts as a discriminated union
// where the `Type` field determines which other fields are valid.
// - For "polygon", `Points` is used.
// - For "circle", `CircleX`, `CircleY`, and `CircleSize` are used.
type Shape struct {
	Type      string
	Points    []Point
	Transform Transform
	Invert    bool
	// Circle-specific fields
	CircleX    float64
	CircleY    float64
	CircleSize float64
}

// GeneratorConfig holds configuration for the generator including cache settings
type GeneratorConfig struct {
	ColorConfig   ColorConfig
	CacheSize     int // Maximum number of items in the LRU cache (default: 1000)
	MaxComplexity int // Maximum geometric complexity score (-1 to disable, 0 for default)
	MaxIconSize   int // Maximum allowed icon size in pixels (0 for default from constants.DefaultMaxIconSize)
}

// DefaultGeneratorConfig returns the default generator configuration
func DefaultGeneratorConfig() GeneratorConfig {
	return GeneratorConfig{
		ColorConfig:   DefaultColorConfig(),
		CacheSize:     1000,
		MaxComplexity: 0, // Use default from constants
		MaxIconSize:   0, // Use default from constants.DefaultMaxIconSize
	}
}

// Generator encapsulates the icon generation logic and provides caching
type Generator struct {
	config      GeneratorConfig
	cache       *lru.Cache[string, *Icon]
	mu          sync.RWMutex
	metrics     CacheMetrics
	sf          singleflight.Group // Prevents thundering herd on cache misses
	maxIconSize int                // Resolved maximum icon size (from config or default)
}

// NewGenerator creates a new Generator with the specified color configuration
// and default cache size of 1000 entries
func NewGenerator(colorConfig ColorConfig) (*Generator, error) {
	generatorConfig := GeneratorConfig{
		ColorConfig: colorConfig,
		CacheSize:   1000,
	}
	return NewGeneratorWithConfig(generatorConfig)
}

// NewGeneratorWithConfig creates a new Generator with the specified configuration
func NewGeneratorWithConfig(config GeneratorConfig) (*Generator, error) {
	if config.CacheSize <= 0 {
		return nil, fmt.Errorf("jdenticon: engine: cache initialization failed: invalid cache size: %d", config.CacheSize)
	}

	config.ColorConfig.Normalize()

	// Resolve the effective maximum icon size
	maxIconSize := config.MaxIconSize
	if maxIconSize == 0 || (maxIconSize < 0 && maxIconSize != -1) {
		maxIconSize = constants.DefaultMaxIconSize
	}
	// If maxIconSize is -1, keep it as -1 to disable the limit

	// Create LRU cache with specified size
	cache, err := lru.New[string, *Icon](config.CacheSize)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: cache initialization failed: %w", err)
	}

	return &Generator{
		config:      config,
		cache:       cache,
		metrics:     CacheMetrics{},
		maxIconSize: maxIconSize,
	}, nil
}

// NewDefaultGenerator creates a new Generator with default configuration
func NewDefaultGenerator() (*Generator, error) {
	generator, err := NewGeneratorWithConfig(DefaultGeneratorConfig())
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: default generator creation failed: %w", err)
	}
	return generator, nil
}

// generateIcon performs the actual icon generation with context support and complexity checking
func (g *Generator) generateIcon(ctx context.Context, hash string, size float64) (*Icon, error) {
	// Check for cancellation before expensive operations
	if err := ctx.Err(); err != nil {
		return nil, err
	}

	// Complexity validation is now handled at the jdenticon package level
	// to ensure proper structured error types are returned

	// Calculate padding and round to nearest integer (matching JavaScript)
	padding := int((0.5 + size*g.config.ColorConfig.IconPadding))
	iconSize := size - float64(padding*paddingMultiple)

	// Calculate cell size and ensure it is an integer (matching JavaScript)
	cell := int(iconSize / gridSize)

	// Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon
	x := int(float64(padding) + iconSize/2 - float64(cell*paddingMultiple))
	y := int(float64(padding) + iconSize/2 - float64(cell*paddingMultiple))

	// Extract hue from hash (last 7 characters)
	hue, err := g.extractHue(hash)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: icon generation failed: %w", err)
	}

	// Generate color theme
	availableColors := GenerateColorTheme(hue, g.config.ColorConfig)

	// Select colors for each shape layer
	selectedColorIndexes, err := g.selectColors(hash, availableColors)
	if err != nil {
		return nil, err
	}

	// Generate shape groups in exact JavaScript order
	shapeGroups := make([]ShapeGroup, 0, numColorSelections)

	// Check for cancellation before rendering shapes
	if err = ctx.Err(); err != nil {
		return nil, err
	}

	// 1. Sides (outer edges) - renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
	var sideShapes []Shape
	err = g.renderShape(ctx, hash, shapeColorIndexSides, hashPosSideShape, hashPosSideRotation,
		[][]int{{1, 0}, {2, 0}, {2, 3}, {1, 3}, {0, 1}, {3, 1}, {3, 2}, {0, 2}},
		x, y, cell, true, &sideShapes)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: icon generation failed: side shapes rendering failed: %w", err)
	}
	if len(sideShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[shapeColorIndexSides]],
			Shapes:    sideShapes,
			ShapeType: "sides",
		})
	}

	// 2. Corners - renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
	var cornerShapes []Shape
	err = g.renderShape(ctx, hash, shapeColorIndexCorners, hashPosCornerShape, hashPosCornerRotation,
		[][]int{{0, 0}, {3, 0}, {3, 3}, {0, 3}},
		x, y, cell, true, &cornerShapes)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: icon generation failed: corner shapes rendering failed: %w", err)
	}
	if len(cornerShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[shapeColorIndexCorners]],
			Shapes:    cornerShapes,
			ShapeType: "corners",
		})
	}

	// 3. Center - renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
	var centerShapes []Shape
	err = g.renderShape(ctx, hash, shapeColorIndexCenter, hashPosCenterShape, hashPosCenterRotation,
		[][]int{{1, 1}, {2, 1}, {2, 2}, {1, 2}},
		x, y, cell, false, &centerShapes)
	if err != nil {
		return nil, fmt.Errorf("jdenticon: engine: icon generation failed: center shapes rendering failed: %w", err)
	}
	if len(centerShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[shapeColorIndexCenter]],
			Shapes:    centerShapes,
			ShapeType: "center",
		})
	}

	return &Icon{
		Hash:   hash,
		Size:   size,
		Config: g.config.ColorConfig,
		Shapes: shapeGroups,
	}, nil
}

// extractHue extracts the hue value from the hash string
func (g *Generator) extractHue(hash string) (float64, error) {
	// Use the last 7 characters of the hash to determine hue
	if len(hash) < hashPosHueLength {
		return 0, fmt.Errorf("jdenticon: engine: hue extraction failed: hash too short for hue extraction")
	}
	hueStr := hash[len(hash)-hashPosHueLength:]
	hueValue64, err := strconv.ParseInt(hueStr, 16, 64)
	if err != nil {
		return 0, fmt.Errorf("jdenticon: engine: hue extraction failed: failed to parse hue '%s': %w", hueStr, err)
	}
	hueValue := int(hueValue64)
	return float64(hueValue) / hueMaxValue, nil
}

// selectColors selects 3 colors from the available color palette
func (g *Generator) selectColors(hash string, availableColors []Color) ([]int, error) {
	if len(availableColors) == 0 {
		return nil, fmt.Errorf("jdenticon: engine: color selection failed: no available colors")
	}

	selectedIndexes := make([]int, numColorSelections)

	for i := 0; i < numColorSelections; i++ {
		indexValue, err := util.ParseHex(hash, hashPosColorStart+i, 1)
		if err != nil {
			return nil, fmt.Errorf("jdenticon: engine: color selection failed: failed to parse color index at position %d: %w", hashPosColorStart+i, err)
		}
		// Defensive check: ensure availableColors is not empty before modulo operation
		// This should never happen due to the check at the start of the function,
		// but provides additional safety for future modifications
		if len(availableColors) == 0 {
			return nil, fmt.Errorf("jdenticon: engine: color selection failed: available colors became empty during selection")
		}
		index := indexValue % len(availableColors)

		// Apply color conflict resolution rules from JavaScript implementation
		if g.isDuplicateColor(index, selectedIndexes[:i], []int{colorDarkGray, colorDarkMain}) || // Disallow dark gray and dark color combo
			g.isDuplicateColor(index, selectedIndexes[:i], []int{colorLightGray, colorLightMain}) { // Disallow light gray and light color combo
			index = colorMidFallback // Use mid color as fallback
		}

		selectedIndexes[i] = index
	}

	return selectedIndexes, nil
}

// isDuplicateColor checks for problematic color combinations
func (g *Generator) isDuplicateColor(index int, selected []int, forbidden []int) bool {
	if !isColorInForbiddenSet(index, forbidden) {
		return false
	}
	return hasSelectedColorInForbiddenSet(selected, forbidden)
}

// isColorInForbiddenSet checks if the given color index is in the forbidden set
func isColorInForbiddenSet(index int, forbidden []int) bool {
	return util.ContainsInt(forbidden, index)
}

// hasSelectedColorInForbiddenSet checks if any selected color is in the forbidden set
func hasSelectedColorInForbiddenSet(selected []int, forbidden []int) bool {
	for _, s := range selected {
		if util.ContainsInt(forbidden, s) {
			return true
		}
	}
	return false
}

// renderShape implements the JavaScript renderShape function exactly with context support
// Shapes are appended directly to the provided destination slice to avoid intermediate allocations
func (g *Generator) renderShape(ctx context.Context, hash string, colorIndex, shapeHashIndex, rotationHashIndex int, positions [][]int, x, y, cell int, isOuter bool, dest *[]Shape) error { //nolint:unparam // colorIndex is passed for API consistency with JavaScript implementation
	shapeIndexValue, err := util.ParseHex(hash, shapeHashIndex, 1)
	if err != nil {
		return fmt.Errorf("jdenticon: engine: shape rendering failed: failed to parse shape index at position %d: %w", shapeHashIndex, err)
	}
	shapeIndex := shapeIndexValue

	var rotation int
	if rotationHashIndex >= 0 {
		rotationValue, err := util.ParseHex(hash, rotationHashIndex, 1)
		if err != nil {
			return fmt.Errorf("jdenticon: engine: shape rendering failed: failed to parse rotation at position %d: %w", rotationHashIndex, err)
		}
		rotation = rotationValue
	}

	for i, pos := range positions {
		// Check for cancellation in the rendering loop
		if err := ctx.Err(); err != nil {
			return err
		}

		// Calculate transform exactly like JavaScript: new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4)
		transformX := float64(x + pos[0]*cell)
		transformY := float64(y + pos[1]*cell)
		var transformRotation int
		if rotationHashIndex >= 0 {
			transformRotation = (rotation + i) % gridSize
		} else {
			// For center shapes (rotationIndex is null), r starts at 0 and increments
			transformRotation = i % gridSize
		}

		transform := NewTransform(transformX, transformY, float64(cell), transformRotation)

		// Get a collector from the pool and reset it
		collector := shapeCollectorPool.Get().(*shapeCollector)
		collector.Reset()

		// Create shape using graphics with pooled collector
		graphics := NewGraphicsWithTransform(collector, transform)

		if isOuter {
			RenderOuterShape(graphics, shapeIndex, float64(cell))
		} else {
			RenderCenterShape(graphics, shapeIndex, float64(cell), float64(i))
		}

		// Append shapes directly to destination slice and return collector to pool
		*dest = append(*dest, collector.shapes...)
		shapeCollectorPool.Put(collector)
	}

	return nil
}

// shapeCollectorPool provides pooled shapeCollector instances for efficient reuse
var shapeCollectorPool = sync.Pool{
	New: func() interface{} {
		// Pre-allocate with reasonable capacity - typical identicon has 4-8 shapes per collector
		return &shapeCollector{shapes: make([]Shape, 0, 8)}
	},
}

// shapeCollector implements Renderer interface to collect shapes during generation
type shapeCollector struct {
	shapes []Shape
}

// Reset clears the shape collector for reuse while preserving capacity
func (sc *shapeCollector) Reset() {
	// Keep capacity but reset length to 0 for efficient reuse
	sc.shapes = sc.shapes[:0]
}

func (sc *shapeCollector) AddPolygon(points []Point) {
	sc.shapes = append(sc.shapes, Shape{
		Type:   "polygon",
		Points: points,
	})
}

func (sc *shapeCollector) AddCircle(topLeft Point, size float64, invert bool) {
	// Store circle with dedicated circle geometry fields
	sc.shapes = append(sc.shapes, Shape{
		Type:       "circle",
		CircleX:    topLeft.X,
		CircleY:    topLeft.Y,
		CircleSize: size,
		Invert:     invert,
	})
}

func getOuterShapeComplexity(shapeIndex int) int {
	index := shapeIndex % 4
	switch index {
	case 0: // Triangle
		return 3
	case 1: // Triangle (different orientation)
		return 3
	case 2: // Rhombus (diamond)
		return 4
	case 3: // Circle
		return 5 // Circles are more expensive to render
	default:
		return 1 // Fallback for unknown shapes
	}
}

// getCenterShapeComplexity returns the complexity score for a center shape type.
// Scoring accounts for multiple geometric elements and cutouts.
func getCenterShapeComplexity(shapeIndex int) int {
	index := shapeIndex % 14
	switch index {
	case 0: // Asymmetric polygon (5 points)
		return 5
	case 1: // Triangle
		return 3
	case 2: // Rectangle
		return 4
	case 3: // Nested rectangles (2 rectangles)
		return 8
	case 4: // Circle
		return 5
	case 5: // Rectangle with triangular cutout (rect + inverted triangle)
		return 7
	case 6: // Complex polygon (6 points)
		return 6
	case 7: // Small triangle
		return 3
	case 8: // Composite shape (2 rectangles + 1 triangle)
		return 11
	case 9: // Rectangle with rectangular cutout (rect + inverted rect)
		return 8
	case 10: // Rectangle with circular cutout (rect + inverted circle)
		return 9
	case 11: // Small triangle (same as 7)
		return 3
	case 12: // Rectangle with rhombus cutout (rect + inverted rhombus)
		return 8
	case 13: // Large circle (conditional rendering)
		return 5
	default:
		return 1 // Fallback for unknown shapes
	}
}

// CalculateComplexity calculates the total geometric complexity for an identicon
// based on the hash string. This provides a fast complexity assessment before
// any expensive rendering operations.
func (g *Generator) CalculateComplexity(hash string) (int, error) {
	totalComplexity := 0

	// Calculate complexity for side shapes (8 positions)
	sideShapeIndexValue, err := util.ParseHex(hash, hashPosSideShape, 1)
	if err != nil {
		return 0, fmt.Errorf("failed to parse side shape index: %w", err)
	}
	sideShapeComplexity := getOuterShapeComplexity(sideShapeIndexValue)
	totalComplexity += sideShapeComplexity * 8 // 8 side positions

	// Calculate complexity for corner shapes (4 positions)
	cornerShapeIndexValue, err := util.ParseHex(hash, hashPosCornerShape, 1)
	if err != nil {
		return 0, fmt.Errorf("failed to parse corner shape index: %w", err)
	}
	cornerShapeComplexity := getOuterShapeComplexity(cornerShapeIndexValue)
	totalComplexity += cornerShapeComplexity * 4 // 4 corner positions

	// Calculate complexity for center shapes (4 positions)
	centerShapeIndexValue, err := util.ParseHex(hash, hashPosCenterShape, 1)
	if err != nil {
		return 0, fmt.Errorf("failed to parse center shape index: %w", err)
	}
	centerShapeComplexity := getCenterShapeComplexity(centerShapeIndexValue)
	totalComplexity += centerShapeComplexity * 4 // 4 center positions

	return totalComplexity, nil
}
