package com.atlassian.stash.internal.util;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.timezone.TimeZoneService;
import com.atlassian.bitbucket.timezone.UserTimeZoneSupplier;
import com.atlassian.bitbucket.util.DateFormatter;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.plugin.webresource.util.HashBuilder;
import com.atlassian.stash.internal.locale.InternalLocaleService;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

@AvailableToPlugins(DateFormatter.class)
@Component("dateFormatter")
public class DefaultDateFormatter implements InternalDateFormatter {

    private static final Duration FIVE_HOURS = Duration.ofHours(5);
    private static final Logger LOG = LoggerFactory.getLogger(DefaultDateFormatter.class);

    private final Properties dateFormatProperties;
    private final I18nService i18nService;
    private final Map<FormatType, DateTimeFormatter> formatters;
    private final InternalLocaleService localeService;
    private final Supplier<Instant> nowSupplier;
    private final String stateHash;
    private final UserTimeZoneSupplier timeZoneService;

    @Autowired
    public DefaultDateFormatter(
            I18nService i18nService,
            InternalApplicationPropertiesService applicationPropertiesService,
            @Value("#{dateFormatProperties}") Properties dateFormatProperties,
            InternalLocaleService localeService,
            TimeZoneService timeZoneService) {
        this(i18nService, applicationPropertiesService, dateFormatProperties, localeService, Instant::now, timeZoneService);
    }

    @VisibleForTesting
    DefaultDateFormatter(I18nService i18nService,
                         InternalApplicationPropertiesService applicationPropertiesService,
                         Properties dateFormatProperties,
                         InternalLocaleService localeService,
                         Supplier<Instant> nowSupplier,
                         TimeZoneService timeZoneService) {
        this.dateFormatProperties = dateFormatProperties;
        this.i18nService = i18nService;
        this.localeService = localeService;
        this.nowSupplier = nowSupplier;
        this.timeZoneService = timeZoneService;

        formatters = generateFormatters(dateFormatProperties, timeZoneService.getDefaultTimeZone(), applicationPropertiesService.getLocale());
        stateHash = calculateState(dateFormatProperties);
    }

    @Override
    public String getStateHash() {
        return stateHash;
    }

    @Nonnull
    @Override
    public String formatDate(@Nonnull Date date, @Nonnull FormatType type) {
        requireNonNull(date, "date");

        return formatInstant(date.toInstant(), type);
    }

    @Nonnull
    @Override
    public String formatInstant(@Nonnull Instant instant, @Nonnull FormatType type) {
        requireNonNull(instant, "instant");
        requireNonNull(type, "type");

        if (type.isAge()) {
            return formatInstantWithRelativeAge(instant, type);
        } else {
            return formatInstantWithFormatString(instant, type);
        }
    }

    @Override
    public String getFormatString(String key) {
        return dateFormatProperties.getProperty(key);
    }

    private static String calculateState(Properties properties) {
        HashBuilder builder = new HashBuilder();
        for (String propertyName : properties.stringPropertyNames()) {
            builder.add(propertyName + StringUtils.stripToEmpty(properties.getProperty(propertyName)));
        }
        return builder.build();
    }

    private static Map<FormatType, DateTimeFormatter> generateFormatters(Properties properties, ZoneId tz, Locale locale) {
        EnumMap<FormatType, DateTimeFormatter> formatterMap = new EnumMap<>(FormatType.class);
        for (FormatType formatType : FormatType.values()) {
            formatterMap.computeIfAbsent(toActualType(formatType), type -> {

                DateTimeFormatter formatter;
                String pattern = StringUtils.stripToNull(properties.getProperty(type.getKey()));
                if (pattern == null) {
                    LOG.warn("Date pattern not found for '{}'. Using a default pattern.", type.getKey());
                    formatter =  DateTimeFormatter.ISO_DATE_TIME;
                } else {
                    try {
                        formatter = DateTimeFormatter.ofPattern(pattern);
                    } catch (IllegalArgumentException e) {
                        LOG.warn("Date pattern '{}' in '{}' is not valid. Using a default pattern.", pattern, type.getKey(), e);
                        formatter =  DateTimeFormatter.ISO_DATE_TIME;
                    }
                }
                return formatter.withLocale(locale).withZone(tz);
            });
        }

        return Collections.unmodifiableMap(formatterMap);
    }

    private static FormatType toActualType(FormatType type) {
        if (type == FormatType.SHORTAGE) {
            return FormatType.SHORT;
        } else if (type == FormatType.LONGAGE) {
            return FormatType.LONG;
        } else {
            return type;
        }
    }

    private String formatInstantWithFormatString(Instant instant, FormatType type) {
        return formatters.get(toActualType(type))
                .withLocale(localeService.getLocale())
                .withZone(timeZoneService.getTimeZone())
                .format(instant);
    }

    private String formatInstantWithRelativeAge(Instant instant, FormatType type) {
        return formatInstantWithRelativeAge(nowSupplier.get(), instant, type);
    }

    private String formatInstantWithRelativeAge(Instant now, Instant time, FormatType type) {
        Duration diff = Duration.between(time, now);
        if (diff.isNegative()) { // date > now
            return formatInstantWithFormatString(time, type);
        }

        long minutes = diff.toMinutes();
        if (minutes < 1) { // now <= date < 1 minute ago
            return i18nService.getMessage(type.getKey() + ".a.moment.ago");
        }
        if (minutes < 2) { // 1 minute ago <= date < 2 minutes ago
            return i18nService.getMessage(type.getKey() + ".one.minute.ago");
        }
        if (minutes < 50) { // 2 minutes ago <= date < 50 minutes ago
            return i18nService.getMessage(type.getKey() + ".x.minutes.ago", minutes);
        }
        if (minutes < 90) { // 50 minutes ago <= date < 90 minutes ago
            return i18nService.getMessage(type.getKey() + ".one.hour.ago");
        }

        long days = diff.toDays();
        if (days < 1) { // 1.5 hours <= date < 24 hours
            //Will be yesterday if > 5 hours and its "yesterday"
            if (diff.compareTo(FIVE_HOURS) > 0 && isYesterday(now, time)) {
                return i18nService.getMessage(type.getKey() + ".one.day.ago");
            }
            long roundUpHours = diff.plusMinutes(30).toHours();
            return i18nService.getMessage(type.getKey() + ".x.hours.ago", roundUpHours);
        }
        if (days < 7) { // 1 days ago <= date < 7 days ago
            //Anything greater >= 48 hours can't be yesterday.
            if (days < 2 && isYesterday(now, time)) {
                return i18nService.getMessage(type.getKey() + ".one.day.ago");
            }
            //The Math.max ensures, if it's not yesterday, we don't say it's one day ago
            return i18nService.getMessage(type.getKey() + ".x.days.ago", Math.max(days, 2));
        }
        if (days < 8) {  // 7 days ago <= date < 8 days ago
            return i18nService.getMessage(type.getKey() + ".one.week.ago");
        }

        return formatInstantWithFormatString(time, type);
    }

    private boolean isYesterday(Instant now, Instant date) {
        ZonedDateTime todayStart = zonedDateTimeFrom(now).with(LocalTime.MIDNIGHT);
        ZonedDateTime zonedDate = zonedDateTimeFrom(date);

        return zonedDate.isBefore(todayStart) && !zonedDate.isBefore(todayStart.minusDays(1));
    }

    private ZonedDateTime zonedDateTimeFrom(Instant instant) {
        return ZonedDateTime.ofInstant(instant, timeZoneService.getTimeZone());
    }
}
