package analytics

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"golang.org/x/sync/errgroup"

	"github.com/nexuscrm/backend/internal/domain/ports"
	pkgErrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// DashboardService manages dashboard execution
type DashboardService struct {
	queryService  ports.QueryService
	reportService *ReportService
	logger        *logging.Logger
}

// NewDashboardService creates a new DashboardService
func NewDashboardService(queryService ports.QueryService, reportService *ReportService, logger *logging.Logger) *DashboardService {
	return &DashboardService{
		queryService:  queryService,
		reportService: reportService,
		logger:        logger,
	}
}

// GetDashboard fetches dashboard definition including components
func (s *DashboardService) GetDashboard(ctx context.Context, dashboardID string, user *models.UserSession) (*models.DashboardDefinition, error) {
	// 1. Fetch Dashboard Record
	// Strategy: Try looking up by developer_name first (safe string comparison).
	// If not found, try looking up by ID.
	// We cannot use OR because the Query Parser strictly validates syntax and might not support OR in this context.

	// A. Try Developer Name
	dashRes, err := s.queryService.Query(ctx, api.QueryRequest{
		ObjectAPIName: constants.TableDashboard,
		Criteria:      []api.QueryCriterion{{Field: "DeveloperName", Op: "=", Val: dashboardID}},
		Limit:         1,
	}, user)

	found := false
	if err == nil && len(dashRes.Records) > 0 {
		found = true
	}

	// B. If not found, try ID
	if !found {
		res, errID := s.queryService.Query(ctx, api.QueryRequest{
			ObjectAPIName: constants.TableDashboard,
			Criteria:      []api.QueryCriterion{{Field: "Id", Op: "=", Val: dashboardID}},
			Limit:         1,
		}, user)
		if errID == nil && len(res.Records) > 0 {
			dashRes = res
			found = true
			err = nil // Clear previous error
		} else if errID != nil {
			// If both failed, return the error from the second attempt (or first if significant)
			if err == nil {
				err = errID
			}
		}
	}

	if err != nil {
		return nil, pkgErrors.NewInternalError("failed to fetch dashboard", err)
	}
	if !found {
		return nil, pkgErrors.NewNotFoundError("Dashboard", dashboardID)
	}
	dashRow := dashRes.Records[0]
	// Resolved ID
	resolvedID := dashRow.GetString("Id")

	// 2. Fetch Components
	compRes, err := s.queryService.Query(ctx, api.QueryRequest{
		ObjectAPIName: constants.TableDashboardComponent,
		Criteria:      []api.QueryCriterion{{Field: "DashboardId", Op: "=", Val: resolvedID}},
		Limit:         100,
	}, user)
	if err != nil {
		return nil, pkgErrors.NewInternalError("failed to fetch components", err)
	}

	// 3. Assemble Definition
	def := &models.DashboardDefinition{
		ID:            dashRow.GetString("Id"),
		Name:          dashRow.GetString("Name"),
		DeveloperName: dashRow.GetString("DeveloperName"),
		Description:   dashRow.GetString("Description"),
		LayoutType:    dashRow.GetString("LayoutType"),
		DashboardType: dashRow.GetString("DashboardType"),
		RunningUserId: dashRow.GetString("RunningUserId"),
		Components:    make([]models.ComponentDefinition, 0, len(compRes.Records)),
	}

	for _, compRow := range compRes.Records {
		comp := models.ComponentDefinition{
			ID:       compRow.GetString("Id"),
			ReportID: compRow.GetString("ReportId"),
			Type:     compRow.GetString("Type"),
			Title:    compRow.GetString("Title"),
		}

		if configStr, ok := compRow["Config"].(string); ok && configStr != "" {
			var config map[string]interface{}
			if err := json.Unmarshal([]byte(configStr), &config); err == nil {
				comp.Config = config
			}
		}
		def.Components = append(def.Components, comp)
	}

	return def, nil
}

// RunDashboard executes all components of a dashboard with optional global filters
func (s *DashboardService) RunDashboard(ctx context.Context, dashboardID string, user *models.UserSession, filters map[string]interface{}) (*models.DashboardResult, error) {
	// 1. Fetch Definition
	def, err := s.GetDashboard(ctx, dashboardID, user)
	if err != nil {
		return nil, err
	}

	// 2. Validate filters against allowed keys
	if err := s.validateFilters(filters); err != nil {
		return nil, err
	}

	// 3. Resolve the effective user for RLS context (Dashboard Running User feature).
	//    - LoggedInUser (default): use the current viewer's RLS.
	//    - SpecifiedUser: all viewers see data as the fixed RunningUserId.
	//    - MyTeamUser: viewers see data as RunningUserId unless they have
	//      ViewMyTeamsDashboards permission (override not yet wired — Phase 3).
	effectiveUser := user
	dashType := def.DashboardType
	if (dashType == "SpecifiedUser" || dashType == "MyTeamUser") && def.RunningUserId != "" {
		runningUser, loadErr := s.loadUserSession(ctx, def.RunningUserId, user)
		if loadErr != nil {
			s.logger.Warn(fmt.Sprintf("dashboard running user not found (dashboardId=%s runningUserId=%s): %v; falling back to logged-in user", dashboardID, def.RunningUserId, loadErr))
		} else {
			effectiveUser = runningUser
		}
	}

	result := &models.DashboardResult{
		DashboardMetadata: *def,
		ComponentData:     make(map[string]*models.ReportResult),
		GeneratedAt:       time.Now(),
	}

	// 4. Parallel Execution using errgroup
	g, ctx := errgroup.WithContext(ctx)
	var mu sync.Mutex

	for _, comp := range def.Components {
		comp := comp // capture loop variable
		g.Go(func() error {
			// Execute Report using effectiveUser for RLS.
			// Load the definition, enable degradation so one bad
			// component doesn't break the whole dashboard.
			rptDef, defErr := s.reportService.GetReportMetadata(ctx, comp.ReportID, effectiveUser)
			if defErr != nil {
				return pkgErrors.NewInternalError(fmt.Sprintf("failed to load report %s", comp.ReportID), defErr)
			}
			rptDef.DegradeOnError = true
			rptRes, err := s.reportService.RunReportDefinition(ctx, rptDef, effectiveUser, filters, 0)
			if err != nil {
				return pkgErrors.NewInternalError(fmt.Sprintf("failed to run report %s", comp.ReportID), err)
			}

			mu.Lock()
			result.ComponentData[comp.ID] = rptRes
			mu.Unlock()
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	}

	return result, nil
}

// loadUserSession fetches a UserSession for the given userID by querying the User table.
// The lookup is performed using the requesting user's credentials so the query itself
// is not subject to the running user's RLS — we only adopt their identity for report
// data queries, not for meta-lookups.
func (s *DashboardService) loadUserSession(ctx context.Context, userID string, requestingUser *models.UserSession) (*models.UserSession, error) {
	res, err := s.queryService.Query(ctx, api.QueryRequest{
		ObjectAPIName: "User",
		Criteria:      []api.QueryCriterion{{Field: "Id", Op: "=", Val: userID}},
		SelectFields:  []string{"Id", "FirstName", "LastName", "Email", "ProfileId", "UserRoleId", "IsActive"},
		Limit:         1,
	}, requestingUser)
	if err != nil {
		return nil, fmt.Errorf("loadUserSession query error: %w", err)
	}
	if len(res.Records) == 0 {
		return nil, fmt.Errorf("running user %s not found", userID)
	}
	row := res.Records[0]

	firstName := row.GetString("FirstName")
	lastName := row.GetString("LastName")
	name := firstName
	if lastName != "" {
		if name != "" {
			name += " "
		}
		name += lastName
	}

	email := row.GetString("Email")
	roleID := row.GetString("UserRoleId")

	session := &models.UserSession{
		ID:        row.GetString("Id"),
		Name:      name,
		ProfileID: row.GetString("ProfileId"),
	}
	if email != "" {
		session.Email = &email
	}
	if roleID != "" {
		session.RoleID = &roleID
	}
	return session, nil
}
