package dashboard

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"
	"tm/pkg/logger"

	"golang.org/x/sync/singleflight"
)

const (
	defaultClosingWindowHours = 168
	defaultTrendDays          = 14
	maxTrendDays              = 90
	defaultCountriesLimit     = 6
	defaultListLimit          = 5
	maxListLimit              = 20
	summaryCacheTTL           = 60 * time.Second
	statisticsCacheTTL        = 60 * time.Second
)

// Service defines dashboard business operations.
type Service interface {
	Summary(ctx context.Context, query SummaryQuery) (*SummaryResponse, error)
	Trend(ctx context.Context, query TrendQuery) (*TrendResponse, error)
	Countries(ctx context.Context, query CountriesQuery) (*CountriesResponse, error)
	NoticeTypes(ctx context.Context) (*NoticeTypesResponse, error)
	ClosingSoon(ctx context.Context, query ClosingSoonQuery) (*ClosingSoonResponse, error)
	Recent(ctx context.Context, query RecentQuery) (*RecentResponse, error)
	Statistics(ctx context.Context, query StatisticsQuery) (*StatisticsReportResponse, error)
}

type cacheEntry[T any] struct {
	expiresAt time.Time
	value     T
}

type service struct {
	repo            Repository
	logger          logger.Logger
	summaryMu       sync.Mutex
	summary         map[int64]cacheEntry[*SummaryResponse]
	summaryGroup    singleflight.Group
	statisticsMu    sync.Mutex
	statistics      map[int]cacheEntry[*StatisticsReportResponse]
	statisticsGroup singleflight.Group
}

// NewService creates a dashboard service.
func NewService(repo Repository, log logger.Logger) Service {
	return &service{
		repo:       repo,
		logger:     log,
		summary:    make(map[int64]cacheEntry[*SummaryResponse]),
		statistics: make(map[int]cacheEntry[*StatisticsReportResponse]),
	}
}

func (s *service) Summary(ctx context.Context, query SummaryQuery) (*SummaryResponse, error) {
	windowHours := closingWindowHours(query.ClosingWindow)
	windowSec := int64(windowHours) * 3600

	if cached, ok := s.cachedSummary(windowSec); ok {
		return cached, nil
	}

	cacheKey := strconv.FormatInt(windowSec, 10)
	out, err, _ := s.summaryGroup.Do(cacheKey, func() (interface{}, error) {
		if cached, ok := s.cachedSummary(windowSec); ok {
			return cached, nil
		}

		s.logger.Info("Fetching dashboard summary", map[string]interface{}{
			"closing_window_hours": windowHours,
		})

		result, err := s.repo.Summary(ctx, windowSec)
		if err != nil {
			s.logger.Error("Failed to fetch dashboard summary", map[string]interface{}{
				"error": err.Error(),
			})
			return nil, fmt.Errorf("dashboard summary: %w", err)
		}

		s.storeSummary(windowSec, result)
		return result, nil
	})
	if err != nil {
		return nil, err
	}

	return out.(*SummaryResponse), nil
}

func (s *service) Trend(ctx context.Context, query TrendQuery) (*TrendResponse, error) {
	days := trendDays(query.Days)
	metric := normalizeTrendMetric(query.Metric)
	startUnix := trendStartUnix(days)

	s.logger.Info("Fetching dashboard trend", map[string]interface{}{
		"days":   days,
		"metric": metric,
	})

	counts, err := s.repo.Trend(ctx, days, metric, startUnix)
	if err != nil {
		s.logger.Error("Failed to fetch dashboard trend", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("dashboard trend: %w", err)
	}

	series := fillTrendSeries(days, counts)

	return &TrendResponse{
		Metric: metric,
		Days:   days,
		Series: series,
	}, nil
}

func (s *service) Countries(ctx context.Context, query CountriesQuery) (*CountriesResponse, error) {
	limit := countriesLimit(query.Limit)

	s.logger.Info("Fetching dashboard countries", map[string]interface{}{
		"limit": limit,
	})

	out, err := s.repo.Countries(ctx, limit)
	if err != nil {
		s.logger.Error("Failed to fetch dashboard countries", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("dashboard countries: %w", err)
	}

	return out, nil
}

func (s *service) NoticeTypes(ctx context.Context) (*NoticeTypesResponse, error) {
	s.logger.Info("Fetching dashboard notice types", nil)

	out, err := s.repo.NoticeTypes(ctx)
	if err != nil {
		s.logger.Error("Failed to fetch dashboard notice types", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("dashboard notice types: %w", err)
	}

	return out, nil
}

func (s *service) ClosingSoon(ctx context.Context, query ClosingSoonQuery) (*ClosingSoonResponse, error) {
	limit := listLimit(query.Limit)
	windowHours := closingWindowHours(query.Window)
	windowSec := int64(windowHours) * 3600

	s.logger.Info("Fetching dashboard closing soon", map[string]interface{}{
		"limit":  limit,
		"window": windowHours,
	})

	items, err := s.repo.ClosingSoon(ctx, limit, windowSec)
	if err != nil {
		s.logger.Error("Failed to fetch dashboard closing soon", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("dashboard closing soon: %w", err)
	}

	return &ClosingSoonResponse{Items: items}, nil
}

func (s *service) Recent(ctx context.Context, query RecentQuery) (*RecentResponse, error) {
	limit := listLimit(query.Limit)

	s.logger.Info("Fetching dashboard recent tenders", map[string]interface{}{
		"limit": limit,
	})

	items, nextCursor, err := s.repo.Recent(ctx, limit, strings.TrimSpace(query.Cursor))
	if err != nil {
		s.logger.Error("Failed to fetch dashboard recent tenders", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("dashboard recent: %w", err)
	}

	return &RecentResponse{
		Items:      items,
		NextCursor: nextCursor,
	}, nil
}

func (s *service) Statistics(ctx context.Context, query StatisticsQuery) (*StatisticsReportResponse, error) {
	days := trendDays(query.Days)
	if cached, ok := s.cachedStatistics(days); ok {
		return cached, nil
	}

	cacheKey := strconv.Itoa(days)
	out, err, _ := s.statisticsGroup.Do(cacheKey, func() (interface{}, error) {
		if cached, ok := s.cachedStatistics(days); ok {
			return cached, nil
		}

		startUnix := trendStartUnix(days)

		s.logger.Info("Fetching dashboard statistics report", map[string]interface{}{
			"days": days,
		})

		result, err := s.repo.Statistics(ctx, days, startUnix)
		if err != nil {
			s.logger.Error("Failed to fetch dashboard statistics report", map[string]interface{}{
				"error": err.Error(),
			})
			return nil, fmt.Errorf("dashboard statistics: %w", err)
		}

		s.storeStatistics(days, result)
		return result, nil
	})
	if err != nil {
		return nil, err
	}

	return out.(*StatisticsReportResponse), nil
}

func (s *service) cachedSummary(windowSec int64) (*SummaryResponse, bool) {
	now := time.Now()

	s.summaryMu.Lock()
	defer s.summaryMu.Unlock()

	entry, ok := s.summary[windowSec]
	if !ok || now.After(entry.expiresAt) {
		if ok {
			delete(s.summary, windowSec)
		}
		return nil, false
	}

	return entry.value, true
}

func (s *service) storeSummary(windowSec int64, value *SummaryResponse) {
	s.summaryMu.Lock()
	defer s.summaryMu.Unlock()

	s.summary[windowSec] = cacheEntry[*SummaryResponse]{
		expiresAt: time.Now().Add(summaryCacheTTL),
		value:     value,
	}
}

func (s *service) cachedStatistics(days int) (*StatisticsReportResponse, bool) {
	now := time.Now()

	s.statisticsMu.Lock()
	defer s.statisticsMu.Unlock()

	entry, ok := s.statistics[days]
	if !ok || now.After(entry.expiresAt) {
		if ok {
			delete(s.statistics, days)
		}
		return nil, false
	}

	return entry.value, true
}

func (s *service) storeStatistics(days int, value *StatisticsReportResponse) {
	s.statisticsMu.Lock()
	defer s.statisticsMu.Unlock()

	s.statistics[days] = cacheEntry[*StatisticsReportResponse]{
		expiresAt: time.Now().Add(statisticsCacheTTL),
		value:     value,
	}
}

func closingWindowHours(hours int) int {
	if hours <= 0 {
		return defaultClosingWindowHours
	}
	return hours
}

func trendDays(days int) int {
	if days <= 0 {
		return defaultTrendDays
	}
	if days > maxTrendDays {
		return maxTrendDays
	}
	return days
}

func countriesLimit(limit int) int {
	if limit <= 0 {
		return defaultCountriesLimit
	}
	return limit
}

func listLimit(limit int) int {
	if limit <= 0 {
		return defaultListLimit
	}
	if limit > maxListLimit {
		return maxListLimit
	}
	return limit
}

func normalizeTrendMetric(metric string) string {
	switch strings.ToLower(strings.TrimSpace(metric)) {
	case "published", "awarded":
		return strings.ToLower(strings.TrimSpace(metric))
	default:
		return "created"
	}
}

func trendStartUnix(days int) int64 {
	now := time.Now().UTC()
	end := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	start := end.AddDate(0, 0, -(days - 1))
	return start.Unix()
}

func fillTrendSeries(days int, counts map[string]int64) []TrendPoint {
	now := time.Now().UTC()
	end := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
	start := end.AddDate(0, 0, -(days-1))

	series := make([]TrendPoint, 0, days)
	for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
		date := d.Format("2006-01-02")
		series = append(series, TrendPoint{
			Date:  date,
			Count: counts[date],
		})
	}

	return series
}
