package schemaversion

import (
	"context"
	"math"
)

const (
	gridColumnCount  = 24.0
	defaultPanelSpan = 4.0
	defaultRowHeight = 250.0
	gridCellHeight   = 30.0
	gridCellVMargin  = 8.0
	minPanelHeight   = gridCellHeight * 3.0
	panelHeightStep  = gridCellHeight + gridCellVMargin
)

// V16 migrates dashboard layout from the old row-based system to the modern grid-based layout.
// This migration follows the exact logic from DashboardMigrator.ts to ensure consistency between frontend and backend.
func V16(_ context.Context, dashboard map[string]interface{}) error {
	dashboard["schemaVersion"] = 16

	upgradeToGridLayout(dashboard)

	return nil
}

func upgradeToGridLayout(dashboard map[string]interface{}) {
	rowsInterface, ok := dashboard["rows"]
	if !ok {
		return
	}

	rows, ok := rowsInterface.([]interface{})
	if !ok {
		return
	}

	// Handle empty rows
	if len(rows) == 0 {
		delete(dashboard, "rows")
		return
	}

	yPos := 0
	widthFactor := gridColumnCount / 12.0

	// Find max panel ID (lines 1014-1021 in TS)
	// Also check top-level panels which may have been assigned IDs by ensurePanelsHaveUniqueIds
	maxPanelID := getMaxPanelID(dashboard, rows)
	nextRowID := maxPanelID + 1

	// Match frontend: dashboard.panels already exists with top-level panels
	// The frontend's this.dashboard.panels is initialized in the constructor with existing panels
	// Then upgradeToGridLayout adds more panels to it

	// Initialize panels array - make a copy to avoid modifying the original
	panels := []interface{}{}
	if existingPanels, ok := dashboard["panels"].([]interface{}); ok && len(existingPanels) > 0 {
		// Copy existing panels to preserve order
		panels = append(panels, existingPanels...)
	}

	// Add special "row" panels if even one row is collapsed, repeated or has visible title (line 1028 in TS)
	showRows := shouldShowRows(rows)

	// Process each row (line 1030 in TS)
	for _, rowInterface := range rows {
		row, ok := rowInterface.(map[string]interface{})
		if !ok {
			continue
		}

		// Skip repeated rows (line 1031-1033 in TS)
		if repeatIteration, hasRepeatIteration := row["repeatIteration"]; hasRepeatIteration && repeatIteration != nil {
			continue
		}

		height := getRowHeight(row)
		rowGridHeight := getGridHeight(height)
		// Check if collapse property exists and get its value
		collapseValue, hasCollapseProperty := row["collapse"]
		isCollapsed := false
		if hasCollapseProperty {
			if b, ok := collapseValue.(bool); ok {
				isCollapsed = b
			}
		}

		var rowPanel map[string]interface{}

		// First pass: assign IDs to panels that don't have them
		panelsInRow, ok := row["panels"].([]interface{})
		if !ok {
			panelsInRow = []interface{}{}
		}

		for _, panelInterface := range panelsInRow {
			panel, ok := panelInterface.(map[string]interface{})
			if !ok {
				continue
			}
			// Assign ID if missing
			if _, hasID := panel["id"]; !hasID {
				panel["id"] = nextRowID
				nextRowID++
			}
		}

		if showRows {
			// add special row panel (lines 1041-1058 in TS)
			rowPanel = map[string]interface{}{
				"id":     nextRowID,
				"type":   "row",
				"title":  GetStringValue(row, "title"),
				"repeat": GetStringValue(row, "repeat"),
				"panels": []interface{}{},
				"gridPos": map[string]interface{}{
					"x": 0,
					"y": yPos,
					"w": int(gridColumnCount),
					"h": rowGridHeight,
				},
			}

			// Match frontend behavior: rowPanel.collapsed = row.collapse (line 1065 in TS)
			// Only set collapsed property if the original row had a collapse property
			if hasCollapseProperty {
				rowPanel["collapsed"] = isCollapsed
			}
			nextRowID++
			yPos++
		}

		rowArea := newRowArea(rowGridHeight, gridColumnCount, yPos)

		// Process all panels in this row (lines 1062-1087 in TS)
		for _, panelInterface := range panelsInRow {
			panel, ok := panelInterface.(map[string]interface{})
			if !ok {
				continue
			}

			// Match frontend logic: panel.span = panel.span || DEFAULT_PANEL_SPAN (line 1082 in TS)
			span := GetFloatValue(panel, "span", 0)
			if span == 0 {
				span = defaultPanelSpan
			}

			panelWidth, panelHeight := calculatePanelDimensionsFromSpan(span, panel, widthFactor, rowGridHeight)

			panelPos := rowArea.getPanelPosition(panelHeight, panelWidth)
			yPos = rowArea.yPos

			// Set gridPos (lines 1072-1077 in TS)
			panel["gridPos"] = map[string]interface{}{
				"x": GetIntValue(panelPos, "x", 0),
				"y": yPos + GetIntValue(panelPos, "y", 0),
				"w": panelWidth,
				"h": panelHeight,
			}
			rowArea.addPanel(panel["gridPos"].(map[string]interface{}))

			// Remove span (line 1080 in TS)
			delete(panel, "span")

			// Match frontend logic: lines 1101-1105 in TS
			if rowPanel != nil && isCollapsed {
				// Add to collapsed row's nested panels (line 1102)
				if rowPanelPanels, ok := rowPanel["panels"].([]interface{}); ok {
					rowPanel["panels"] = append(rowPanelPanels, panel)
				}
			} else {
				// Add directly to panels array like frontend (line 1104)
				panels = append(panels, panel)
			}
		}

		// Add row panel after regular panels from this row (lines 1108-1110 in TS)
		if rowPanel != nil {
			panels = append(panels, rowPanel)
		}

		// Update yPos (lines 1093-1095 in TS)
		if rowPanel == nil || !isCollapsed {
			yPos += rowGridHeight
		}
	}

	// Update the dashboard
	dashboard["panels"] = panels
	delete(dashboard, "rows")
}

// rowArea represents dashboard row filled by panels
type rowArea struct {
	area   []int
	yPos   int
	height int
}

func newRowArea(height int, width int, rowYPos int) *rowArea {
	area := make([]int, width)
	return &rowArea{
		area:   area,
		yPos:   rowYPos,
		height: height,
	}
}

func (r *rowArea) reset() {
	for i := range r.area {
		r.area[i] = 0
	}
}

func (r *rowArea) addPanel(gridPos map[string]interface{}) {
	x := GetIntValue(gridPos, "x", 0)
	y := GetIntValue(gridPos, "y", 0)
	w := GetIntValue(gridPos, "w", 0)
	h := GetIntValue(gridPos, "h", 0)

	for i := x; i < x+w && i < len(r.area); i++ {
		newHeight := y + h - r.yPos
		if newHeight > r.area[i] {
			r.area[i] = newHeight
		}
	}
}

func (r *rowArea) getPanelPosition(panelHeight int, panelWidth int) map[string]interface{} {
	var startPlace, endPlace int
	found := false

	// Find available space from right to left
	for i := len(r.area) - 1; i >= 0; i-- {
		if r.height-r.area[i] > 0 {
			if !found {
				endPlace = i
				found = true
			} else {
				if i < len(r.area)-1 && r.area[i] <= r.area[i+1] {
					startPlace = i
				} else {
					break
				}
			}
		} else {
			break
		}
	}

	if found && endPlace-startPlace >= panelWidth-1 {
		// Find max height in the range
		yPos := 0
		for i := startPlace; i <= endPlace && i < len(r.area); i++ {
			if r.area[i] > yPos {
				yPos = r.area[i]
			}
		}
		return map[string]interface{}{
			"x": startPlace,
			"y": yPos,
		}
	}

	// Wrap to next row
	r.yPos += r.height
	r.reset()
	return r.getPanelPosition(panelHeight, panelWidth)
}

func getMaxPanelID(dashboard map[string]interface{}, rows []interface{}) int {
	maxID := 0
	hasValidID := false

	// Check top-level panels first (these may have been assigned IDs by ensurePanelsHaveUniqueIds)
	if panels, ok := dashboard["panels"].([]interface{}); ok {
		for _, panelInterface := range panels {
			if panel, ok := panelInterface.(map[string]interface{}); ok {
				if id := GetIntValue(panel, "id", 0); id > 0 {
					hasValidID = true
					if id > maxID {
						maxID = id
					}
				}
			}
		}
	}

	// Also check panels inside rows
	for _, rowInterface := range rows {
		if row, ok := rowInterface.(map[string]interface{}); ok {
			if panels, ok := row["panels"].([]interface{}); ok {
				for _, panelInterface := range panels {
					if panel, ok := panelInterface.(map[string]interface{}); ok {
						if id := GetIntValue(panel, "id", 0); id > 0 {
							hasValidID = true
							if id > maxID {
								maxID = id
							}
						}
					}
				}
			}
		}
	}

	// If no valid IDs found, return 0 (matches frontend behavior)
	if !hasValidID {
		return 0
	}

	return maxID
}

func shouldShowRows(rows []interface{}) bool {
	for _, rowInterface := range rows {
		if row, ok := rowInterface.(map[string]interface{}); ok {
			collapse := GetBoolValue(row, "collapse")
			showTitle := GetBoolValue(row, "showTitle")
			repeat := GetStringValue(row, "repeat")

			if collapse || showTitle || repeat != "" {
				return true
			}
		}
	}
	return false
}

func getRowHeight(row map[string]interface{}) float64 {
	if height, ok := row["height"]; ok {
		if h, ok := ConvertToFloat(height); ok {
			return h
		}
	}
	return defaultRowHeight
}

func getGridHeight(height float64) int {
	if height < minPanelHeight {
		height = minPanelHeight
	}
	return int(math.Ceil(height / panelHeightStep))
}

func calculatePanelDimensionsFromSpan(span float64, panel map[string]interface{}, widthFactor float64, defaultHeight int) (int, int) {
	// span should already be normalized by caller (line 1082 in DashboardMigrator.ts)

	if minSpan, hasMinSpan := panel["minSpan"]; hasMinSpan {
		if minSpanFloat, ok := ConvertToFloat(minSpan); ok && minSpanFloat > 0 {
			panel["minSpan"] = int(math.Min(float64(gridColumnCount), (float64(gridColumnCount)/12.0)*minSpanFloat))
		}
	}

	// Match frontend logic: Math.floor(panel.span) * widthFactor (line 914 in DashboardMigrator.ts)
	// Frontend floors the span FIRST, then multiplies by widthFactor
	panelWidth := int(math.Floor(span)) * int(widthFactor)
	panelHeight := defaultHeight

	if panelHeightValue, hasHeight := panel["height"]; hasHeight {
		if h, ok := ConvertToFloat(panelHeightValue); ok {
			panelHeight = getGridHeight(h)
		}
	}

	return panelWidth, panelHeight
}
