package dashboard

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

	"golang.org/x/sync/errgroup"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

// Service manages Salesforce-compatible CRM dashboard execution.
type Service struct {
	queryService        contract.Query
	reportService       contract.Reports
	effectiveUserPolicy DashboardEffectiveUserPolicy
}

type ServiceOption func(*Service)

func WithDashboardEffectiveUserPolicy(policy DashboardEffectiveUserPolicy) ServiceOption {
	return func(s *Service) {
		s.effectiveUserPolicy = policy
	}
}

func (s *Service) SetDashboardEffectiveUserPolicy(policy DashboardEffectiveUserPolicy) {
	if s == nil {
		return
	}
	s.effectiveUserPolicy = policy
}

// NewService creates a new Salesforce-compatible CRM dashboard service.
func NewService(queryService contract.Query, reportService contract.Reports, opts ...ServiceOption) *Service {
	svc := &Service{
		queryService:  queryService,
		reportService: reportService,
	}
	for _, opt := range opts {
		opt(svc)
	}
	return svc
}

// GetDashboard fetches dashboard definition including components
func (s *Service) GetDashboard(ctx context.Context, dashboardID string, user *models.UserSession) (*Definition, 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, queryspec.QuerySpec{
		ObjectAPIName: constants.TableDashboard,
		Filter:        queryspec.FieldPredicate("DeveloperName", "=", 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, queryspec.QuerySpec{
			ObjectAPIName: constants.TableDashboard,
			Filter:        queryspec.FieldPredicate("Id", "=", 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, newInternalError("failed to fetch dashboard", err)
	}
	if !found {
		return nil, sharedErrors.NewNotFoundWireError(fmt.Sprintf("Dashboard with ID '%s' not found", dashboardID))
	}
	dashRow := dashRes.Records[0]
	// Resolved ID
	resolvedID := recordString(dashRow, "Id")

	// 2. Fetch Components
	compRes, err := s.queryService.Query(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableDashboardComponent,
		Filter:        queryspec.FieldPredicate("DashboardId", "=", resolvedID),
		Limit:         100,
	}, user)
	if err != nil {
		return nil, newInternalError("failed to fetch components", err)
	}

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

	for _, compRow := range compRes.Records {
		comp := ComponentDefinition{
			ID:       recordString(compRow, "Id"),
			ReportID: recordString(compRow, "ReportId"),
			Type:     recordString(compRow, "Type"),
			Title:    recordString(compRow, "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 *Service) RunDashboard(ctx context.Context, dashboardID string, user *models.UserSession, filters map[string]interface{}) (*Result, 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 through product policy.
	effectiveUser, err := s.resolveDashboardEffectiveUser(ctx, dashboardID, def, user)
	if err != nil {
		return nil, err
	}

	result := &Result{
		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 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 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
}

func recordString(record contract.Record, key string) string {
	if record == nil {
		return ""
	}
	value, ok := record[key]
	if !ok || value == nil {
		return ""
	}
	if s, ok := value.(string); ok {
		return s
	}
	return ""
}

func (s *Service) resolveDashboardEffectiveUser(ctx context.Context, dashboardID string, def *Definition, viewer *models.UserSession) (*models.UserSession, error) {
	if s.effectiveUserPolicy == nil {
		return viewer, nil
	}
	effectiveUser, err := s.effectiveUserPolicy.EffectiveUser(ctx, DashboardEffectiveUserRequest{
		DashboardID: dashboardID,
		Definition:  def,
		Viewer:      viewer,
	})
	if err != nil {
		return nil, err
	}
	if effectiveUser == nil {
		return viewer, nil
	}
	return effectiveUser, nil
}

type internalError struct {
	message string
	cause   error
}

func newInternalError(message string, cause error) *internalError {
	return &internalError{message: message, cause: cause}
}

func (e *internalError) Error() string {
	if e == nil {
		return ""
	}
	if e.cause != nil {
		return fmt.Sprintf("internal error: %s (caused by: %v)", e.message, e.cause)
	}
	return fmt.Sprintf("internal error: %s", e.message)
}

func (e *internalError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.cause
}

func (e *internalError) InternalCondition() bool { return true }
