import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static lib_refining.PrlConstants.*;

/**
 * {@summary Performs time-based flow smoothing for monthly plans with optional transition periods.}
 * <p>
 * <p>Converts monthly plan values into hourly flow rates for the current month.
 *
 * <p>Supports smooth transitions between previous → current month and current → next month.
 *
 * <p>Guarantees total sum equals specified monthly plan within numerical tolerance.
 *
 * <p>Uses linear interpolation for transitions and constant values for non-transition periods.
 *
 * <p>Applies automatic correction if accumulated values deviate from target plan.
 *
 * <p>Used in {@code SourceElement} when smoothing option is enabled.
 * <p>
 * #LibRefiningApi
 */
public class SmoothingFlows implements Serializable{
	
	private static final int TRANSITION_INTERVALS_DEFAULT = 4;
	private static final int TRANSITION_SIZE = 10;
	
	private boolean isFlowSmoothing;
	
	private double previousMonthPlan;
	private double currentMonthPlan;
	private double nextMonthPlan;	
	
	private Date currentDate;
	
	private int durationOfTransition;
	private int transitionIntervals;	
	private boolean isFirstTimeCall;
	
	private List<Double> hourlyValues;
	

/**
 * {@summary Creates flow smoothing model for a monthly plan.}
 * <p>
 * #LibRefiningApi
 *
 * @param isFlowSmoothing enables transition smoothing
 * @param previousMonthPlan previous month total value
 * @param currentMonthPlan current month total value
 * @param nextMonthPlan next month total value
 * @param transitionDuration transition duration (hours)
 * @param transitionInterval number of interpolation intervals
 * @param currentDate reference date (within current month)
 * @param isFirstTimeCall indicates first calculation run
 */
	public SmoothingFlows(boolean isFlowSmoothing, 
						  double previousMonthPlan, 
						  double currentMonthPlan, 
						  double nextMonthPlan, 
						  int transitionDuration,  
						  int transitionInterval, 
						  Date currentDate, 
						  boolean isFirstTimeCall) {
		
		this.isFlowSmoothing = isFlowSmoothing;		
		this.previousMonthPlan = previousMonthPlan;
		this.currentMonthPlan = currentMonthPlan;
		this.nextMonthPlan = nextMonthPlan;
		this.currentDate = currentDate;
		this.isFirstTimeCall = isFirstTimeCall;

		boolean availableIntervals = transitionInterval > 0  && transitionInterval <= TRANSITION_SIZE;
		this.transitionIntervals = availableIntervals
										? transitionInterval 
										: TRANSITION_INTERVALS_DEFAULT;
		this.durationOfTransition = calculateRoundedDuration(transitionDuration, transitionIntervals);
	
		computeHourlyFlowPlan();
	}
	
	
/**
 * {@summary Computes hourly flow distribution for the current month.}
 * <p>
 * <p>Includes base uniform distribution, optional start/end transitions, and post-calculation normalization.
 * <p>
 * #LibRefiningApi
 */
	private void computeHourlyFlowPlan() {		
		final int HOURS_PER_DAY = 24;
		
		int remainingDays = getRemainingDaysInMonth(currentDate);
		int totalHours = HOURS_PER_DAY * remainingDays;
		
        hourlyValues = new ArrayList<> (totalHours);
        
        // Calculate base hourly value without transitions (even distribution)
        double baseHourlyValue = currentMonthPlan / totalHours;
        
        // Initialize all hours with base value  
        for (int i = 0; i < totalHours; i++) {
            hourlyValues.add(baseHourlyValue);
        }
                
        if (isFlowSmoothing) {
        	
        	if (previousMonthPlan < MIN_THRESHOLD_VALUE && !isFirstTimeCall) 
        		interpolateTransition(0, durationOfTransition, 0.0, baseHourlyValue);
                
        	// Calculate total remaining hours in current month (days * 24)       	
        	int currentMonthNumber = 1 + getMonth(currentDate);
	        Date startOfNextMonth = java.sql.Date.valueOf(YearMonth.of(getYear(currentDate), currentMonthNumber).plusMonths(1).atDay(1));   
	        double nextMonthHourlyValue = nextMonthPlan / (getRemainingDaysInMonth(startOfNextMonth) * HOURS_PER_DAY);
	      
	        
	        // Calculate transition volume and adjust main period values
	        double transitionVolume = durationOfTransition * baseHourlyValue;
	        double remainingVolume = currentMonthPlan - transitionVolume;
	        
	        // Recalculate hourly value for main period (excluding transition)
	        double mainPartHourlyValue = remainingVolume / (totalHours - durationOfTransition);
	        
	        // Apply end transition (smooth change to next month's value)
	        interpolateTransition(totalHours - durationOfTransition, 
					        	  durationOfTransition, 
					        	  mainPartHourlyValue, 
					        	  nextMonthHourlyValue);
	        
	        // Verify and correct any deviation from planned total
	        double actualSum = 0;
	        for (double v : hourlyValues) actualSum += v;
	        
	        double correctionFactor = currentMonthPlan / actualSum;
	        if (Math.abs(correctionFactor - 1.0) > MIN_THRESHOLD_VALUE) {
	            for (int i = 0; i < hourlyValues.size(); i++) {
	                hourlyValues.set(i, hourlyValues.get(i) * correctionFactor);
	            }
	        }
        }
	} 


/**
 * {@summary Applies linear interpolation between two flow values over a transition period.}
 * <p>
 * <p>Divides transition into equal intervals and assigns constant value per interval.
 * <p>
 * #LibRefiningApi
 *
 * @param startHour start index
 * @param transitionDuration duration in hours
 * @param startValue initial value
 * @param endValue target value
 */
	private void interpolateTransition(int startHour, int transitionDuration, double startValue, double endValue) {
	    final double HALF_STEP = 0.5;
		for (int interval = 0; interval < transitionIntervals; interval++) {
	    	int step = transitionDuration / transitionIntervals;
	        int localStart = interval * step;
	        int localEnd = (interval == transitionIntervals - 1) 
					       	? transitionDuration 
					       	: localStart + step;

	        double ratio = (interval + HALF_STEP) / (double) transitionIntervals;
	        double value = startValue + (endValue - startValue) * ratio;

	        for (int hour = localStart; hour < localEnd; hour++) {
	            int index = startHour + hour;
	            if (index >= 0 && index < hourlyValues.size()) {
	                hourlyValues.set(index, value);
	            }
	        }
	    }
	}

	
/**
 * {@summary Calculates remaining days in the month from a given date.}
 * <p>
 * #LibRefiningApi
 *
 * @param startDate reference date (inclusive)
 * @return number of days remaining in month
 * @throws NullPointerException if startDate is null
 */	
	private int getRemainingDaysInMonth(Date startDate) {
	    LocalDate startLocal = Instant.ofEpochMilli(startDate.getTime())
	            					  .atZone(ZoneId.systemDefault())
	            					  .toLocalDate();
	    LocalDate firstNextMonth = startLocal.plusMonths(1).withDayOfMonth(1);	   
	    return (int) ChronoUnit.DAYS.between(startLocal, firstNextMonth);
	}
	
	
/**
 * {@summary Rounds transition duration to nearest multiple of interval length.}
 * <p>
 * #LibRefiningApi
 *
 * @param transitionDuration requested duration in hours
 * @param transitionInterval number of intervals (minimum 1)
 * @return adjusted duration divisible by interval count
 */
	private static int calculateRoundedDuration(int transitionDuration, int transitionInterval) {
	    if (transitionInterval == 0) transitionInterval = TRANSITION_INTERVALS_DEFAULT;
	    
	    long remainder = transitionDuration % transitionInterval;
	    if (remainder != 0) {
	        return (int) (transitionDuration + (transitionInterval - remainder));
	    }
	    return (int) transitionDuration;
	}
	
	
/**
 * {@summary Calculates hours elapsed from month start to specified datetime.}
 * <p>
 * #LibRefiningApi
 *
 * @param date target datetime (must be non-null)
 * @return hours passed since month start including fractional hours
 * @throws IllegalArgumentException if date is null
 */	
    private int getHoursFromBeginOfTheMonth(Date date) {
        if (date == null) {
	        String errorMessage = IS_ENGLISH 
						            ? "Date cannot be null" 
						            : "Дата не может быть null";
            throw new IllegalArgumentException(errorMessage);
        }
        
        Instant instant = date.toInstant();
        LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        LocalDateTime firstOfMonth = dateTime
						        		.withDayOfMonth(1)
						        		.withHour(0)
						        		.withMinute(0)
						        		.withSecond(0)
						        		.withNano(0);       
        
        long seconds = Duration.between(firstOfMonth, dateTime).getSeconds();
        return (int) Math.round(seconds / CONVERT_SECOND_TO_HOUR);
    }
	
    
/**
 * {@summary Returns flow value for specified date within current month.}
 * <p>
 * #LibRefiningApi
 *
 * @param date target date
 * @return flow value (units/hour)
 * @throws IndexOutOfBoundsException if calculated index is outside available range
 */
    public double getCurrentFlow(Date date) {
		int index = getHoursFromBeginOfTheMonth(date);
		
		return hourlyValues.get(index);
	}

	
/**
 * {@summary Returns an unmodifiable view of the calculated hourly flow rates.}
 * <p>
 * #LibRefiningApi
 *
 * @return immutable list of hourly values (units/hour) for current month
 */	
	public List<Double> getHourlyValues() {
	    return Collections.unmodifiableList(hourlyValues);
	}
	
		
/**
 * {@summary Calculates duration of continuous flow period with same value.}
 * <p>
 * Excludes transition zones when determining stable segments.
 * <p>
 * #LibRefiningApi
 *
 * @param value target flow rate to match (units/hour)
 * @param date reference starting position in month
 * @return duration in hours
 */
	public int getConstantFlowSpanLength(double value, Date date) {
	    int index = getHoursFromBeginOfTheMonth(date);
	    int endSearchIndex = hourlyValues.size();

	    // Outside the end-of-month transition zone
	    if (index < endSearchIndex - durationOfTransition) {
	        endSearchIndex -= durationOfTransition;
	    }

	    // Current value no longer matches the requested flow
	    if (Math.abs(hourlyValues.get(index) - value) >= MIN_THRESHOLD_VALUE) {
	        return 0;
	    }

	    // Find the next change of flow
	    for (int i = index + 1; i < endSearchIndex; i++) {
	        if (Math.abs(hourlyValues.get(i) - value) >= MIN_THRESHOLD_VALUE) {
	            return i - index;
	        }
	    }

	    // No changes until the end of the stable period
	    return endSearchIndex - index;
	}	
	
}