package rest

import (
	"encoding/json"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/analytics"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

const (
	dashboardTableName = constants.TableDashboard
	defaultLayoutType  = "FixedLayout"
)

type cachedDashboardResult struct {
	result    *models.DashboardResult
	timestamp time.Time
}

type DashboardHandler struct {
	svc            *analytics.DashboardService
	svcMgr         *services.ServiceManager
	executionCache map[string]*cachedDashboardResult
	cacheMutex     sync.RWMutex
	stopCleanup    chan struct{}
}

// NewDashboardHandler creates a new DashboardHandler
func NewDashboardHandler(svcMgr *services.ServiceManager) *DashboardHandler {
	handler := &DashboardHandler{
		svc:            svcMgr.DashboardSvc,
		svcMgr:         svcMgr,
		executionCache: make(map[string]*cachedDashboardResult),
		stopCleanup:    make(chan struct{}, 1),
	}

	// Start periodic cleanup goroutine
	go handler.startCleanup()
	return handler
}

// startCleanup starts a background goroutine that periodically cleans up expired cache entries
func (h *DashboardHandler) startCleanup() {
	ticker := time.NewTicker(1 * time.Minute) // Clean up every minute
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			h.cleanupExpiredEntries()
		case <-h.stopCleanup:
			return
		}
	}
}

// cleanupExpiredEntries removes expired cache entries to prevent memory leaks
func (h *DashboardHandler) cleanupExpiredEntries() {
	h.cacheMutex.Lock()
	defer h.cacheMutex.Unlock()

	now := time.Now()
	for dashboardID, cached := range h.executionCache {
		if now.Sub(cached.timestamp) > 5*time.Minute {
			delete(h.executionCache, dashboardID)
		}
	}
}

// ListDashboards handles GET /analytics/dashboards
func (h *DashboardHandler) ListDashboards(c *gin.Context) {

	user := MustGetUser(c)

	// Parse optional limit parameter
	limit := 200
	if l := c.Query("limit"); l != "" {
		if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 2000 {
			limit = parsed
		}
	}

	ctx := c.Request.Context()
	result, err := h.svcMgr.QuerySvc.QueryWithFilter(ctx, dashboardTableName, "", user, "Name", "ASC", limit)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	dashboards := make([]DashboardResponse, 0, len(result.Records))
	for _, r := range result.Records {
		dashboards = append(dashboards, toDashboardResponse(r))
	}

	c.JSON(http.StatusOK, gin.H{
		"dashboards": dashboards,
		"totalSize":  len(dashboards),
	})
}

// DashboardResponse represents a dashboard in Salesforce wire format
type DashboardResponse struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	Label         string `json:"label"`
	DeveloperName string `json:"developerName"`
	FolderID      string `json:"folderId,omitempty"`
	Description   string `json:"description,omitempty"`
	LayoutType    string `json:"layoutType,omitempty"`
}

func toDashboardResponse(r models.SObject) DashboardResponse {
	return DashboardResponse{
		ID:            utils.GetSObjectString(r, "Id"),
		Name:          utils.GetSObjectString(r, "Name"),
		Label:         utils.GetSObjectString(r, "Name"),
		DeveloperName: utils.GetSObjectString(r, "DeveloperName"),
		FolderID:      utils.GetSObjectString(r, "FolderId"),
		Description:   utils.GetSObjectString(r, "Description"),
		LayoutType:    utils.GetSObjectString(r, "LayoutType"),
	}
}

// ComponentRequest represents a dashboard component in a create request
type ComponentRequest struct {
	ReportID string                 `json:"reportId"`
	Type     string                 `json:"type"`
	Title    string                 `json:"title"`
	Config   map[string]interface{} `json:"config,omitempty"`
}

// CreateDashboardRequest represents a dashboard creation request
type CreateDashboardRequest struct {
	Name          string             `json:"name" binding:"required"`
	DeveloperName string             `json:"developerName,omitempty"`
	FolderID      string             `json:"folderId" binding:"required"`
	Description   string             `json:"description,omitempty"`
	LayoutType    string             `json:"layoutType,omitempty"`
	Components    []ComponentRequest `json:"components,omitempty"`
}

// CreateDashboard handles POST /analytics/dashboards
func (h *DashboardHandler) CreateDashboard(c *gin.Context) {

	user := MustGetUser(c)

	var req CreateDashboardRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeRequiredFieldMissing, err.Error()))
		return
	}

	// Generate DeveloperName if not provided using sanitization utility
	developerName := req.DeveloperName
	if developerName == "" {
		developerName = utils.SanitizeDeveloperName(req.Name)
	}

	// Default layout type
	layoutType := req.LayoutType
	if layoutType == "" {
		layoutType = defaultLayoutType
	}

	ctx := c.Request.Context()

	// Creating a dashboard in a folder requires Manage access on that folder
	if h.svcMgr.FolderAccess != nil && !h.svcMgr.FolderAccess.CheckAccess(ctx, user, req.FolderID, permissions.FolderAccessManage) {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "You do not have Manage access on the target folder"))
		return
	}

	data := models.SObject{
		"Name":          req.Name,
		"DeveloperName": developerName,
		"FolderId":      req.FolderID,
		"LayoutType":    layoutType,
	}
	if req.Description != "" {
		data["Description"] = req.Description
	}

	createdRecord, err := h.svcMgr.Persistence.Insert(ctx, dashboardTableName, data, user)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	dashboardID := utils.GetSObjectString(createdRecord, "Id")

	// Persist components if provided
	for _, comp := range req.Components {
		compData := models.SObject{
			"DashboardId": dashboardID,
			"ReportId":    comp.ReportID,
			"Type":        comp.Type,
			"Title":       comp.Title,
		}
		if len(comp.Config) > 0 {
			if configJSON, err := json.Marshal(comp.Config); err == nil {
				compData["Config"] = string(configJSON)
			}
		}
		if _, err := h.svcMgr.Persistence.Insert(ctx, constants.TableDashboardComponent, compData, user); err != nil {
			RespondAppError(c, err)
			return
		}
	}

	c.JSON(http.StatusCreated, toDashboardResponse(createdRecord))
}

// UpdateDashboardRequest represents a dashboard update request
type UpdateDashboardRequest struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	LayoutType  *string `json:"layoutType,omitempty"`
}

// UpdateDashboard handles PATCH /analytics/dashboards/:dashboardId
func (h *DashboardHandler) UpdateDashboard(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	var req UpdateDashboardRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidField, err.Error()))
		return
	}

	ctx := c.Request.Context()
	updates := models.SObject{}
	if req.Name != nil {
		updates["Name"] = *req.Name
	}
	if req.Description != nil {
		updates["Description"] = *req.Description
	}
	if req.LayoutType != nil {
		updates["LayoutType"] = *req.LayoutType
	}

	if len(updates) == 0 {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidField, "No fields to update"))
		return
	}

	err := h.svcMgr.Persistence.Update(ctx, dashboardTableName, dashboardID, updates, user)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	record, err := h.svcMgr.Persistence.Retrieve(ctx, dashboardTableName, dashboardID, user)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	c.JSON(http.StatusOK, toDashboardResponse(record))
}

// DeleteDashboard handles DELETE /analytics/dashboards/:dashboardId
func (h *DashboardHandler) DeleteDashboard(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	ctx := c.Request.Context()
	err := h.svcMgr.Persistence.Delete(ctx, dashboardTableName, dashboardID, user)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	c.Status(http.StatusNoContent)
}

// GetDashboardSalesforce returns definition unwrapped (SF Style)
func (h *DashboardHandler) GetDashboardSalesforce(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	def, err := h.svc.GetDashboard(c.Request.Context(), dashboardID, user)
	if err != nil {
		RespondAppError(c, err)
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"dashboardMetadata": def,
		"componentData":     map[string]interface{}{},
	})
}

// RefreshDashboard executes dashboard
func (h *DashboardHandler) RefreshDashboard(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	// Parse optional filters from query parameters
	var filterParams struct {
		StartDate string `form:"startDate"`
		EndDate   string `form:"endDate"`
	}
	if err := c.ShouldBindQuery(&filterParams); err != nil {
		RespondAppError(c, err)
		return
	}

	filters := make(map[string]interface{})
	if filterParams.StartDate != "" {
		filters["startDate"] = filterParams.StartDate
	}
	if filterParams.EndDate != "" {
		filters["endDate"] = filterParams.EndDate
	}

	res, err := h.svc.RunDashboard(c.Request.Context(), dashboardID, user, filters)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	c.JSON(http.StatusOK, res)
}

// RegisterSalesforceRoutes registers dashboard routes for /services/data/vXX.0/analytics
func (h *DashboardHandler) RegisterSalesforceRoutes(group *gin.RouterGroup) {
	group.GET("/dashboards", h.ListDashboards)
	group.GET("/dashboards/:dashboardId", h.GetDashboardSalesforce)
	group.POST("/dashboards", h.CreateDashboard)
	group.PATCH("/dashboards/:dashboardId", h.UpdateDashboard)
	group.PUT("/dashboards/:dashboardId", h.RefreshDashboardPUT) // Salesforce uses PUT for refresh
	group.DELETE("/dashboards/:dashboardId", h.DeleteDashboard)
	group.POST("/dashboards/:dashboardId/refresh", h.RefreshDashboard)
	// Status endpoint for polling
	group.GET("/dashboards/:dashboardId/status", h.GetDashboardStatus)
}

// RefreshDashboardPUT handles PUT /analytics/dashboards/:dashboardId
// Salesforce Pattern: PUT initiates async refresh and returns statusUrl for polling
func (h *DashboardHandler) RefreshDashboardPUT(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	// Initiate async refresh in background (for now we do synchronous but return statusUrl pattern)
	// In a full implementation this would start a background job
	ctx := c.Request.Context()
	result, err := h.svc.RunDashboard(ctx, dashboardID, user, nil)
	if err != nil {
		RespondAppError(c, err)
		return
	}

	// Store the result in the cache for status polling
	h.cacheMutex.Lock()
	h.executionCache[dashboardID] = &cachedDashboardResult{
		result:    result,
		timestamp: time.Now(),
	}
	h.cacheMutex.Unlock()

	// Return statusUrl for polling per Salesforce spec
	// The statusUrl points to GET /analytics/dashboards/:id/status
	statusUrl := "/services/data/v63.0/analytics/dashboards/" + dashboardID + "/status"
	c.JSON(http.StatusOK, gin.H{
		"statusUrl": statusUrl,
	})
}

// GetDashboardStatus handles GET /analytics/dashboards/:dashboardId/status
// Returns the dashboard refresh status for polling
func (h *DashboardHandler) GetDashboardStatus(c *gin.Context) {

	user := MustGetUser(c)

	dashboardID, ok := RequireParam(c, "dashboardId")
	if !ok {
		return
	}

	// Check if we have a cached result from the refresh operation
	h.cacheMutex.RLock()
	cached, exists := h.executionCache[dashboardID]
	h.cacheMutex.RUnlock()

	if !exists || cached == nil {
		// If no cached result exists, return an appropriate error or status
		def, _ := h.svc.GetDashboard(c.Request.Context(), dashboardID, user)
		c.JSON(http.StatusOK, gin.H{
			"status":            "NotFound",
			"dashboardMetadata": def,
			"componentData":     map[string]interface{}{},
			"errorMessage":      "Dashboard execution result not found in cache",
		})
		return
	}

	// Check if the cached result is too old (e.g., older than 5 minutes)
	if time.Since(cached.timestamp) > 5*time.Minute {
		// Cached result is too old, remove it and return error status
		h.cacheMutex.Lock()
		delete(h.executionCache, dashboardID)
		h.cacheMutex.Unlock()

		def, _ := h.svc.GetDashboard(c.Request.Context(), dashboardID, user)
		c.JSON(http.StatusOK, gin.H{
			"status":            "Expired",
			"dashboardMetadata": def,
			"componentData":     map[string]interface{}{},
			"errorMessage":      "Cached dashboard result has expired",
		})
		return
	}

	// Return the cached result
	c.JSON(http.StatusOK, gin.H{
		"status":            "Success",
		"dashboardMetadata": cached.result.DashboardMetadata,
		"componentData":     cached.result.ComponentData,
	})
}
