package unstable_plan

import (
	"time"

	"github.com/pingcap/log"
	"github.com/samber/lo"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// Service encapsulates the business logic and execution context.
type Service struct {
	db                  *gorm.DB
	taskCreateTimeEpoch int64
	offsetHours         int
}

func NewService(db *gorm.DB, taskCreateTimeEpoch int64, offsetHours int) *Service {
	return &Service{db: db, offsetHours: offsetHours, taskCreateTimeEpoch: taskCreateTimeEpoch}
}

func (s *Service) DoComputation() ([]Model, error) {
	res, err := s.queryStatements()
	if err != nil {
		return nil, err
	}
	stmtMap, err := s.calculateSuggestedPlan(res)
	if err != nil {
		return nil, err
	}
	plans, err := s.queryPlans()
	if err != nil {
		return nil, err
	}
	joinedStmtMap := s.joinPlansToStatements(stmtMap, plans)
	return s.buildResults(joinedStmtMap), nil
}

func (s *Service) CheckPrivilege() (*Model, error) {
	query := s.db.
		Select("digest").
		Table(StatementsTable).
		Limit(1)

	var res Model
	if err := query.Take(&res).Error; err != nil {
		return nil, err
	}
	return &res, nil
}

// queryStatements queries the statement summary table and aggregate the data against customer's TiDB cluster, grouped by (schema_name, digest).
func (s *Service) queryStatements() (result []*Model, err error) {
	// Calculate time range.
	endTime := s.taskCreateTimeEpoch
	startTime := endTime - int64(s.offsetHours*3600)

	// Generate select statement from model.
	selectStmt, err := GenSelectStmt(s.db)
	if err != nil {
		return nil, err
	}

	// Do query.
	query := s.db.
		Select(selectStmt).
		Table(StatementsTable).
		// Return true if two range overlap.
		Where("summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time >= FROM_UNIXTIME(?)", endTime, startTime).
		Group("schema_name, digest")

	var tempRes []Model
	err = query.Find(&tempRes).Error
	if err != nil {
		return nil, err
	}

	// Map to pointer type for later modification.
	result = lo.Map(tempRes, func(m Model, _ int) *Model {
		return &m
	})

	return result, nil
}

func (s *Service) calculateSuggestedPlan(stmts []*Model) (map[string]*Model, error) {
	// Do computation and build stmt map keyed by (schema_name, digest).
	stmtMap := lo.Associate(stmts, func(m *Model) (string, *Model) {
		// Nanosecond precision is enough. It's safe to truncate decimal places.
		m.M = float64(m.AggMaxLatency-m.AggMinLatency) / float64(time.Second)
		if m.AggMinLatency <= 0 {
			m.HasSuggestedPlan = lo.ToPtr(false)
			log.Warn("min latency <= 0, skip", zap.String("digest", m.Digest), zap.Int64("min_latency", m.AggMinLatency))
			return m.GetKey(), m
		}
		m.N = float64(m.AggMaxLatency) / float64(m.AggMinLatency)
		m.HasSuggestedPlan = lo.ToPtr(m.AggPlanCount >= 2 && (m.M >= 1 || m.N >= 2))
		if 10 <= m.N || 10 <= m.M {
			m.Impact = ImpactHigh
		} else if (5 <= m.N && m.N < 10) || (5 <= m.M && m.M < 10) {
			m.Impact = ImpactMedium
		} else if (2 <= m.N && m.N < 5) || (1 <= m.M && m.M < 5) {
			m.Impact = ImpactLow
		}
		return m.GetKey(), m
	})

	return stmtMap, nil
}

func (s *Service) queryPlans() ([]*Model, error) {
	// Calculate time range.
	endTime := s.taskCreateTimeEpoch
	startTime := endTime - int64(s.offsetHours*3600)

	// Generate select statement from model.
	selectStmt, err := GenSelectStmt(s.db)
	if err != nil {
		return nil, err
	}

	query := s.db.
		Select(selectStmt).
		Table(StatementsTable).
		Where("summary_begin_time <= FROM_UNIXTIME(?) AND summary_end_time >= FROM_UNIXTIME(?)", endTime, startTime).
		Group("schema_name, digest, plan_digest")

	// Do query.
	var tempRes []Model
	if err := query.Find(&tempRes).Error; err != nil {
		return nil, err
	}

	// Map to pointer type for later modification.
	result := lo.Map(tempRes, func(m Model, _ int) *Model {
		return &m
	})

	return result, err
}

func (s *Service) joinPlansToStatements(stmtMap map[string]*Model, plans []*Model) map[string]*Model {
	// Manually join the plans with statement map.
	lo.ForEach(plans, func(m *Model, _ int) {
		// Insert into plan list in the statement.
		stmt, ok := stmtMap[m.GetKey()]
		if !ok {
			log.Warn("statement not found in stmtMap", zap.String("key", m.GetKey()))
			return
		}
		stmt.DetailPlans = append(stmt.DetailPlans, m)

		// Update the suggested plan.
		if stmt.HasSuggestedPlan != nil && *stmt.HasSuggestedPlan {
			if stmt.SuggestedPlan == nil || stmt.SuggestedPlan.AggAvgLatency > m.AggAvgLatency {
				stmt.SuggestedPlan = m
				// stmt.SuggestedPlan and stmt.DetailPlans[x] are both pointers and refer to the same obj, so we only need to modify the value once.
				m.IsSuggested = lo.ToPtr(true)
			}
		}
	})

	return stmtMap
}

func (s *Service) buildResults(stmtMap map[string]*Model) []Model {
	return lo.MapToSlice(stmtMap, func(_ string, m *Model) Model {
		return *m
	})
}
