// service.go defines application read ports and use-case orchestration.
// Layer: internal/app core business-facing API.
package app

import (
	"context"
	"strings"
	"time"

	"gitea.maximumdirect.net/ejr/weatherfeeder/model"
)

// Repository defines outbound data access used by weatherapi use cases.
type Repository interface {
	LatestObservation(ctx context.Context) (*model.WeatherObservation, error)
	LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error)
	LatestNarrativeForecast(ctx context.Context) (*model.WeatherForecastRun, error)
	LatestForecastDiscussion(ctx context.Context) (*model.WeatherForecastDiscussion, error)
	LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error)
	LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error)
	LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
	LatestConvectiveOutlookRun(ctx context.Context) (*model.WeatherOutlookRun, error)
	CurrentConditions(ctx context.Context, observationWindowMinutes int) (*CurrentConditions, error)
}

// OutlookFilter selects outlook entries from the latest convective outlook run.
type OutlookFilter struct {
	Day              *int
	OutlookType      string
	ContainsLocation *bool
	ActiveAt         *time.Time
}

// Service provides weather read use-cases.
type Service struct {
	repo Repository
}

func NewService(repo Repository) *Service {
	return &Service{repo: repo}
}

func (s *Service) LatestObservation(ctx context.Context) (*model.WeatherObservation, error) {
	return s.repo.LatestObservation(ctx)
}

func (s *Service) LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
	return s.repo.LatestHourlyForecast(ctx)
}

func (s *Service) LatestNarrativeForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
	return s.repo.LatestNarrativeForecast(ctx)
}

func (s *Service) LatestForecastDiscussion(ctx context.Context) (*model.WeatherForecastDiscussion, error) {
	return s.repo.LatestForecastDiscussion(ctx)
}

func (s *Service) LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error) {
	return s.repo.LatestWeatherStoryRun(ctx)
}

func (s *Service) LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error) {
	return s.repo.LatestWeatherStory(ctx)
}

func (s *Service) LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error) {
	return s.repo.LatestAlertRun(ctx)
}

func (s *Service) LatestActiveAlertRun(ctx context.Context, activeAt time.Time) (*model.WeatherAlertRun, error) {
	run, err := s.repo.LatestAlertRun(ctx)
	if err != nil {
		return nil, err
	}
	if run == nil {
		return nil, nil
	}

	out := cloneAlertRun(run)
	alerts := out.Alerts[:0]
	for _, alert := range out.Alerts {
		if isActiveAlert(alert, activeAt) {
			alerts = append(alerts, alert)
		}
	}
	out.Alerts = alerts
	return out, nil
}

func (s *Service) LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error) {
	run, err := s.repo.LatestConvectiveOutlookRun(ctx)
	if err != nil {
		return nil, err
	}
	if run == nil {
		return nil, nil
	}

	out := cloneOutlookRun(run)
	outlooks := out.Outlooks[:0]
	for _, outlook := range out.Outlooks {
		if matchesOutlookFilter(outlook, filter) {
			outlooks = append(outlooks, outlook)
		}
	}
	out.Outlooks = outlooks
	out.Discussions = filterOutlookDiscussions(out.Discussions, out.Outlooks)
	return out, nil
}

func (s *Service) CurrentConditions(ctx context.Context) (*CurrentConditions, error) {
	return s.repo.CurrentConditions(ctx, ObservationWindowMinutesDefault)
}

func matchesOutlookFilter(outlook model.WeatherOutlook, filter OutlookFilter) bool {
	if filter.Day != nil && outlook.Day != *filter.Day {
		return false
	}
	if filter.OutlookType != "" && outlook.OutlookType != normalizeOutlookType(filter.OutlookType) {
		return false
	}
	if filter.ContainsLocation != nil && outlook.ContainsLocation != *filter.ContainsLocation {
		return false
	}
	if filter.ActiveAt != nil && (filter.ActiveAt.Before(outlook.ValidFrom) || !filter.ActiveAt.Before(outlook.ValidTo)) {
		return false
	}
	return true
}

func normalizeOutlookType(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func cloneAlertRun(run *model.WeatherAlertRun) *model.WeatherAlertRun {
	out := *run
	out.Latitude = copyFloat64(run.Latitude)
	out.Longitude = copyFloat64(run.Longitude)
	if run.Alerts != nil {
		out.Alerts = make([]model.WeatherAlert, len(run.Alerts))
		for i := range run.Alerts {
			out.Alerts[i] = cloneAlert(run.Alerts[i])
		}
	}
	return &out
}

func cloneAlert(alert model.WeatherAlert) model.WeatherAlert {
	out := alert
	out.Sent = copyTime(alert.Sent)
	out.Effective = copyTime(alert.Effective)
	out.Onset = copyTime(alert.Onset)
	out.Expires = copyTime(alert.Expires)
	if alert.References != nil {
		out.References = make([]model.AlertReference, len(alert.References))
		for i := range alert.References {
			out.References[i] = cloneAlertReference(alert.References[i])
		}
	}
	return out
}

func cloneAlertReference(ref model.AlertReference) model.AlertReference {
	out := ref
	out.Sent = copyTime(ref.Sent)
	return out
}

func isActiveAlert(alert model.WeatherAlert, activeAt time.Time) bool {
	if strings.EqualFold(strings.TrimSpace(alert.MessageType), "Cancel") {
		return false
	}
	if alert.Effective != nil && activeAt.Before(*alert.Effective) {
		return false
	}
	if alert.Expires != nil && !activeAt.Before(*alert.Expires) {
		return false
	}
	return true
}

func cloneOutlookRun(run *model.WeatherOutlookRun) *model.WeatherOutlookRun {
	out := *run
	out.Latitude = copyFloat64(run.Latitude)
	out.Longitude = copyFloat64(run.Longitude)
	out.IssuedAt = copyTime(run.IssuedAt)
	if run.Outlooks != nil {
		out.Outlooks = make([]model.WeatherOutlook, len(run.Outlooks))
		for i := range run.Outlooks {
			out.Outlooks[i] = cloneOutlook(run.Outlooks[i])
		}
	}
	if run.Discussions != nil {
		out.Discussions = make([]model.WeatherOutlookDiscussion, len(run.Discussions))
		for i := range run.Discussions {
			out.Discussions[i] = cloneOutlookDiscussion(run.Discussions[i])
		}
	}
	return &out
}

func cloneOutlook(outlook model.WeatherOutlook) model.WeatherOutlook {
	out := outlook
	out.SeverityRank = copyInt(outlook.SeverityRank)
	if outlook.Geometry != nil {
		out.Geometry = append([]byte(nil), outlook.Geometry...)
	}
	return out
}

func cloneOutlookDiscussion(discussion model.WeatherOutlookDiscussion) model.WeatherOutlookDiscussion {
	out := discussion
	out.UpdatedAt = copyTime(discussion.UpdatedAt)
	return out
}

func filterOutlookDiscussions(discussions []model.WeatherOutlookDiscussion, outlooks []model.WeatherOutlook) []model.WeatherOutlookDiscussion {
	if len(outlooks) == 0 {
		return []model.WeatherOutlookDiscussion{}
	}

	retainedDays := make(map[int]struct{}, len(outlooks))
	for _, outlook := range outlooks {
		retainedDays[outlook.Day] = struct{}{}
	}

	out := discussions[:0]
	for _, discussion := range discussions {
		if _, ok := retainedDays[discussion.Day]; ok {
			out = append(out, discussion)
		}
	}
	return out
}

func copyFloat64(value *float64) *float64 {
	if value == nil {
		return nil
	}
	out := *value
	return &out
}

func copyInt(value *int) *int {
	if value == nil {
		return nil
	}
	out := *value
	return &out
}

func copyTime(value *time.Time) *time.Time {
	if value == nil {
		return nil
	}
	out := *value
	return &out
}
