import { dateI18n } from '@wordpress/date';
import {
	addDays,
	addMonths,
	addYears,
	endOfDay,
	endOfMonth,
	endOfYear,
	isSameDay,
	startOfDay,
	startOfMonth,
	startOfYear
} from 'date-fns';
import { __ } from '@wordpress/i18n';

/**
 * Returns a formatted string that represents the relative time between two dates
 * @param {Date | number} relativeDate - The date to compare or a UTC timestamp
 * @param {Date}          date         - The reference date, defaults to the current date
 * @return {string} The relative time string
 */
const getRelativeTime = ( relativeDate, date = new Date() ) => {

	// if relativeDate is a number, we assume it is an UTC timestamp
	if ( 'number' === typeof relativeDate ) {

		// convert to date object
		relativeDate = new Date( relativeDate * 1000 );
	}
	if ( ! ( relativeDate instanceof Date ) ) {

		// invalid date, probably still loading
		return '-';
	}
	const units = {
		year: 24 * 60 * 60 * 1000 * 365,
		month: ( 24 * 60 * 60 * 1000 * 365 ) / 12,
		day: 24 * 60 * 60 * 1000,
		hour: 60 * 60 * 1000,
		minute: 60 * 1000,
		second: 1000
	};
	const rtf = new Intl.RelativeTimeFormat( 'en', { numeric: 'auto' });
	const elapsed = relativeDate - date;

	// "Math.abs" accounts for both "past" & "future" scenarios
	for ( const u in units ) {
		if ( Math.abs( elapsed ) > units[u] || 'second' === u ) {
			return rtf.format( Math.round( elapsed / units[u]), u );
		}
	}

	return '-';
};

/**
 * Calculates the percentage of a value from the total and returns it as a formatted string or a number
 * @param {number}  val          - The value to calculate the percentage of
 * @param {number}  total        - The total value
 * @param {boolean} shouldFormat - If true, returns the percentage as a formatted string, otherwise as a number
 * @return {string | number} The formatted percentage or the raw percentage
 */
const getPercentage = ( val, total, shouldFormat = true ) => {
	val = Number( val );
	total = Number( total );
	let percentage = val / total;
	if ( isNaN( percentage ) ) {
		percentage = 0;
	}
	return shouldFormat ?
		new Intl.NumberFormat( undefined, {
				style: 'percent',
				maximumFractionDigits: 1
			}).format( percentage ) :
		percentage;
};

/**
 * Calculates the percentage change between two values and returns an object with the formatted percentage and status
 * @param {number} currValue - The current value
 * @param {number} prevValue - The previous value
 * @return {Object} An object with a formatted percentage and a status ('positive' or 'negative')
 */
function getChangePercentage( currValue, prevValue ) {
	currValue = Number( currValue );
	prevValue = Number( prevValue );

	const change = {};
	let percentage = ( currValue - prevValue ) / prevValue;
	if ( isNaN( percentage ) ) {
		percentage = 0;
	}

	change.val = new Intl.NumberFormat( undefined, {
		style: 'percent',
		maximumFractionDigits: 1,
		signDisplay: 'exceptZero'
	}).format( percentage );
	change.status = 0 < percentage ? 'positive' : 'negative';

	if ( percentage === Infinity ) {
		change.val = '';
		change.status = 'positive';
	}

	return change;
}

function getAbsoluteChangePercentage( currValue, prevValue ) {
	currValue = Number( currValue );
	prevValue = Number( prevValue );
	const change = {};
	let percentage = ( currValue - prevValue ) / 100;
	if ( isNaN( percentage ) ) {
		percentage = 0;
	}

	change.val = new Intl.NumberFormat( undefined, {
		style: 'percent',
		maximumFractionDigits: 1,
		signDisplay: 'exceptZero'
	}).format( percentage );

	change.status = 0 < percentage ? 'positive' : 'negative';

	if ( percentage === Infinity ) {
		change.val = '';
		change.status = 'positive';
	}

	return change;
}

/**
 * Calculates the bounce percentage of bounced sessions and total sessions
 * @param {number}  bounced_sessions - The number of bounced sessions
 * @param {number}  sessions         - The total number of sessions
 * @param {boolean} shouldFormat     - If true, returns the bounce percentage as a formatted string, otherwise as a number
 * @return {string | number} The formatted bounce percentage or the raw bounce percentage
 */
function getBouncePercentage( bounced_sessions, sessions, shouldFormat = true ) {
	bounced_sessions = Number( bounced_sessions );
	sessions = Number( sessions );
	return getPercentage(
		bounced_sessions,
		sessions + bounced_sessions,
		shouldFormat
	);
}

/**
 * Formats a Unix timestamp as a date string, using the site's locale and wp date format
 * @param {number} unixTimestamp - The Unix timestamp to format
 * @return {string} The formatted date string
 */
const formatUnixToDate = ( unixTimestamp ) => {
	return dateI18n(
		burst_settings.date_format,
		new Date( unixTimestamp * 1000 )
	);
};

/**
 * Formats a Unix timestamp using Intl.
 *
 * @param {number} unixTimestamp - Unix timestamp in seconds
 *
 * @return {string}
 */
const formatUnixToTime = ( unixTimestamp ) => {
	const date = new Date(
		unixTimestamp * 1000
	);

	return new Intl.DateTimeFormat(
		undefined,
		{
			timeStyle: 'short'
		}
	).format( date );
};

/**
 * Formats a Unix timestamp as a date and time string, using the site's locale and wp date/time format
 *
 * @param {number} unixTimestamp - The Unix timestamp to format
 *
 * @return {string} The formatted date and time string
 */
const formatUnixToDateTime = ( unixTimestamp ) => {
	return dateI18n(
		`${ burst_settings.date_format } \\a\\t ${ burst_settings.time_format }`,
		unixTimestamp * 1000
	);
};


/**
 * Check if a date is valid
 * @param {string | number} date - The date to check
 * @return {boolean} True if the date is valid, false otherwise
 */
const isValidDate = ( date ) => {
	const MIN_START_DATE = 1640995200 * 1000; // January 1, 2022 in Unix timestamp
	return (
		date && ( 'number' === typeof date || Date.parse( date ) >= MIN_START_DATE )
	);
};

/**
 * Converts a date to a Unix timestamp in milliseconds
 * @param {string | number} date - The date to convert
 * @return {number} The Unix timestamp in milliseconds
 */
const toUnixTimestampMillis = ( date ) => {
	if ( 'number' === typeof date ) {

		// If the number is 10 digits long, assume it's in seconds and convert to milliseconds
		return 10 === date.toString().length ? date * 1000 : date;
	}

	// If it's a string, parse it to get milliseconds
	return Date.parse( date );
};

/**
 * Formats a duration given in milliseconds as a time string in the format 'HH:mm:ss'
 * @param {number} timeInMilliSeconds - The duration in milliseconds
 * @return {string} The formatted time string
 */
function formatTime( timeInMilliSeconds = 0 ) {
	let timeInSeconds = Number( timeInMilliSeconds );
	if ( isNaN( timeInSeconds ) ) {
		timeInSeconds = 0;
	}

	const seconds = Math.floor( timeInSeconds / 1000 );
	const hours = Math.floor( seconds / 3600 );
	const minutes = Math.floor( ( seconds - hours * 3600 ) / 60 );
	const remainingSeconds = seconds - hours * 3600 - minutes * 60;

	const zeroPad = ( num ) => {
		if ( isNaN( num ) ) {
			return '00';
		}
		return String( num ).padStart( 2, '0' );
	};

	// if hours is 0, return minutes and seconds
	if ( 0 === hours ) {
		return [ minutes, remainingSeconds ].map( zeroPad ).join( ':' );
	}

	return [ hours, minutes, remainingSeconds ].map( zeroPad ).join( ':' );
}

/**
 * Formats a number using compact notation with the specified number of decimal places
 * @param {number} value    - The number to format
 * @param {number} decimals - The number of decimal places to use
 * @return {string} The formatted number
 */
function formatNumber( value, decimals = 1 ) {
	value = Number( value );
	if ( isNaN( value ) ) {
		value = 0;
	}

	// if value is smaller than 1000, return the number without decimals.
	if ( 1000 > value ) {
		decimals = 0;
	}
	return new Intl.NumberFormat( undefined, {
		style: 'decimal',
		notation: 'compact',
		compactDisplay: 'short',
		maximumFractionDigits: decimals
	}).format( value );
}

/**
 * Formats a percentage value with the specified number of decimal places
 * @param {number} value    - The percentage value (not multiplied by 100)
 * @param {number} decimals - The number of decimal places to use
 * @return {string} The formatted percentage
 */
function formatPercentage( value, decimals = 1 ) {
	value = Number( value );
	if ( isNaN( value ) ) {
		value = 0;
	}
	if ( 0 === value ) {
		return '0%';
	}
	if ( 0 < value && 0.1 > value ) {
		return '<0.1%';
	}

	// For other values, format as percent with given decimals
	return new Intl.NumberFormat( undefined, {
		style: 'percent',
		maximumFractionDigits: decimals
	}).format( value / 100 );
}

/**
 * Returns the name of a country based on its country code. If undefined return Unknown
 * @param {string} countryCode countryCode - The country code
 * @return {string} The country name
 */
function getCountryName( countryCode ) {
	if ( countryCode ) {
		return (
			burst_settings.countries[countryCode.toUpperCase()] ||
			__( 'Not set', 'burst-statistics' )
		);
	}
	return __( 'Unknown', 'burst-statistics' );
}

function getContinentName( continentCode ) {
	if ( continentCode ) {
		return (
			burst_settings.continents[continentCode.toUpperCase()] ||
			__( 'Not set', 'burst-statistics' )
		);
	}
	return __( 'Unknown', 'burst-statistics' );
}

function getDateWithOffset( currentDate = new Date() ) {

	// get client's timezone offset in minutes
	const clientTimezoneOffsetMinutes = currentDate.getTimezoneOffset();

	// convert client's timezone offset from minutes to seconds
	const clientTimezoneOffsetSeconds = clientTimezoneOffsetMinutes * -60;

	// get current unix timestamp
	const currentUnix = Math.floor( currentDate.getTime() / 1000 );

	// add burst_settings.gmt_offset x hour and client's timezone offset in
	// seconds to currentUnix
	const currentUnixWithOffsets =
		currentUnix +
		burst_settings.gmt_offset * 3600 -
		clientTimezoneOffsetSeconds;

	const currentDateWithOffset = new Date( currentUnixWithOffsets * 1000 );

	return currentDateWithOffset;
}
const currentDateWithOffset = getDateWithOffset();

const availableRanges = {
	today: {
		label: __( 'Today', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay( currentDateWithOffset ),
			endDate: endOfDay( currentDateWithOffset )
		})
	},
	yesterday: {
		label: __( 'Yesterday', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay( addDays( currentDateWithOffset, -1 ) ),
			endDate: endOfDay( addDays( currentDateWithOffset, -1 ) )
		})
	},
	'last-7-days': {
		label: __( 'Last 7 days', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay( addDays( currentDateWithOffset, -7 ) ),
			endDate: endOfDay( addDays( currentDateWithOffset, -1 ) )
		})
	},
	'last-week': {
		label: __( 'Last week', 'burst-statistics' ),
		range: () => {
			const daysFromSunday = currentDateWithOffset.getDay();
			const startOfThisWeek = addDays( currentDateWithOffset, -daysFromSunday );
			return {
				startDate: startOfDay( addDays( startOfThisWeek, -7 ) ),
				endDate: endOfDay( addDays( startOfThisWeek, -1 ) )
			};
		}
	},
	'last-30-days': {
		label: __( 'Last 30 days', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay( addDays( currentDateWithOffset, -30 ) ),
			endDate: endOfDay( addDays( currentDateWithOffset, -1 ) )
		})
	},
	'last-90-days': {
		label: __( 'Last 90 days', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay( addDays( currentDateWithOffset, -90 ) ),
			endDate: endOfDay( addDays( currentDateWithOffset, -1 ) )
		})
	},
	'last-month': {
		label: __( 'Last month', 'burst-statistics' ),
		range: () => ({
			startDate: startOfMonth( addMonths( currentDateWithOffset, -1 ) ),
			endDate: endOfMonth( addMonths( currentDateWithOffset, -1 ) )
		})
	},
	'week-to-date': {
		label: __( 'Week to date', 'burst-statistics' ),
		range: () => ({
			startDate: startOfDay(
				addDays( currentDateWithOffset, -currentDateWithOffset.getDay() )
			),
			endDate: endOfDay( currentDateWithOffset )
		})
	},
	'month-to-date': {
		label: __( 'Month to date', 'burst-statistics' ),
		range: () => ({
			startDate: startOfMonth( currentDateWithOffset ),
			endDate: endOfDay( currentDateWithOffset )
		})
	},
	'year-to-date': {
		label: __( 'Year to date', 'burst-statistics' ),
		range: () => ({
			startDate: startOfYear( currentDateWithOffset ),
			endDate: endOfDay( currentDateWithOffset )
		})
	},
	'last-year': {
		label: __( 'Last year', 'burst-statistics' ),
		range: () => ({
			startDate: startOfYear( addYears( currentDateWithOffset, -1 ) ),
			endDate: endOfYear( addYears( currentDateWithOffset, -1 ) )
		})
	}
};

const getAvailableRanges = ( selectedRanges ) => {
	return Object.values( selectedRanges )
		.filter( Boolean )
		.map( ( value ) => {
			const range = availableRanges[value];
			range.isSelected = isSelected;
			return range;
		});
};

const getAvailableRangesWithKeys = ( selectedRanges ) => {
	const ranges = {};
	Object.keys( availableRanges ) // Get the keys from the availableRanges object
		.filter( ( key ) => selectedRanges.includes( key ) ) // Filter the keys based on selectedRanges
		.forEach( ( key ) => {
			ranges[key] = {

				// Assign a new object to the key on the ranges object
				...availableRanges[key] // Spread the properties from the range object
			};
		});
	return ranges;
};

const getDisplayDates = ( startDate, endDate ) => {

	// format is based on user's locale
	return {
		startDate: startDate ? formatDate( new Date( startDate ) ) : '',
		endDate: endDate ? formatDate( new Date( endDate ) ) : ''
	};
};

function isSelected( range ) {
	const definedRange = this.range();
	return (
		isSameDay( range.startDate, definedRange.startDate ) &&
		isSameDay( range.endDate, definedRange.endDate )
	);
}

/**
 * Creates a value formatter function based on metric options.
 * @param {string} metric        - The metric key.
 * @param {Object} metricOptions - The metric options object.
 * @return {Function} A value formatter function.
 */
function createValueFormatter( metric, metricOptions = {}) {
	if ( ! metric || ! metricOptions[metric]) {
		return ( d ) => formatNumber( d );
	}

	const { isPercentage, isTime, precision, suffix } = metricOptions[metric];

	return ( value ) => {
		if ( null === value || value === undefined ) {
			return '';
		}

		if ( isPercentage ) {
			return formatPercentage( value, precision );
		}

		if ( isTime ) {
			return formatTime( value );
		}

		let formatted = formatNumber( value, precision );
		if ( suffix ) {
			formatted += suffix;
		}
		return formatted;
	};
}

/**
 * Formats a currency value using Intl.NumberFormat and returns the currency symbol
 * @param {string} currency - The currency code (e.g., 'USD', 'EUR').
 * @param {number} value    - The currency value to format.
 *
 * @return {string} The formatted currency symbol.
 */
function formatCurrency( currency, value ) {
	return new Intl.NumberFormat( undefined, {
		style: 'currency',
		currency,
		maximumFractionDigits: 2,
		minimumFractionDigits: 2,
		trailingZeroDisplay: 'stripIfInteger'
	}).format( value );
}

/**
 * Formats a currency value in compact form (e.g. €100k, $2.5M)
 *
 * @param {string} currency - The currency code (e.g., 'USD', 'EUR').
 * @param {number} value    - The currency value to format.
 *
 * @return {string} The compact formatted currency value.
 */
function formatCurrencyCompact( currency, value ) {
	return new Intl.NumberFormat( undefined, {
		style: 'currency',
		currency,
		notation: 'compact',
		compactDisplay: 'short',
		maximumFractionDigits: 1
	}).format( value );
}

/**
 * Formats a date string for display (e.g., "September 1, 2025").
 * Uses Intl.DateTimeFormat for proper localization.
 *
 * @param {string | Date} dateInput - The date string (YYYY-MM-DD) or Date object.
 * @return {string} The formatted date string, or empty string if invalid.
 */
function formatDate( dateInput ) {
	if ( ! dateInput ) {
		return '';
	}

	try {
		const date = dateInput instanceof Date ? dateInput : new Date( dateInput );

		// Check if the date is valid.
		if ( isNaN( date.getTime() ) ) {
			return '';
		}

		return new Intl.DateTimeFormat( undefined, {
			month: 'long',
			day: 'numeric',
			year: 'numeric'
		}).format( date );
	} catch {
		return '';
	}
}

/**
 * Formats a date string for display (e.g., "September 1, 2025 12:00:00").
 * Uses Intl.DateTimeFormat for proper localization.
 *
 * @param {string | Date} dateInput - The date string (YYYY-MM-DD) or Date object.
 * @return {string} The formatted date string, or empty string if invalid.
 */
function formatDateAndTime( dateInput ) {
	if ( ! dateInput ) {
		return '';
	}

	try {
		const date = dateInput instanceof Date ? dateInput : new Date( dateInput );

		// Check if the date is valid.
		if ( isNaN( date.getTime() ) ) {
			return '';
		}

		return new Intl.DateTimeFormat( undefined, {
			month: 'long',
			day: 'numeric',
			year: 'numeric',
			hour: 'numeric',
			minute: 'numeric',
			second: 'numeric'
		}).format( date );
	} catch {
		return '';
	}
}

/**
 * Formats a date string for short display (e.g., "Dec 23, 2025").
 * Uses Intl.DateTimeFormat for proper localization.
 *
 * @param {string | Date} dateInput - The date string (YYYY-MM-DD) or Date object.
 * @return {string} The formatted date string, or empty string if invalid.
 */
function formatDateShort( dateInput ) {
	if ( ! dateInput ) {
		return '';
	}

	try {
		const date = dateInput instanceof Date ? dateInput : new Date( dateInput );

		// Check if the date is valid.
		if ( isNaN( date.getTime() ) ) {
			return '';
		}

		return new Intl.DateTimeFormat( undefined, {
			month: 'short',
			day: 'numeric',
			year: 'numeric'
		}).format( date );
	} catch {
		return '';
	}
}

/**
 * Format a duration in seconds to a human-readable string.
 *
 * Examples:
 *   0     → "0s"
 *   30    → "30s"
 *   90    → "1m 30s"
 *   120   → "2m"
 *   3600  → "1h"
 *   3660  → "1h 1m"
 *
 * @param {number} seconds - Duration in seconds.
 * @return {string} Human-readable duration.
 */
const formatDuration = ( seconds ) => {
	if ( 0 === seconds ) {
		return '0s';
	}
	if ( 0 === seconds % 3600 ) {
		return `${seconds / 3600}h`;
	}
	if ( 0 === seconds % 60 ) {
		return `${seconds / 60}m`;
	}
	if ( 60 > seconds ) {
		return `${seconds}s`;
	}
	const m = Math.floor( seconds / 60 );
	const s = seconds % 60;
	return `${m}m ${s}s`;
};

export {
	getRelativeTime,
	getPercentage,
	getChangePercentage,
	getAbsoluteChangePercentage,
	getBouncePercentage,
	formatUnixToDate,
	isValidDate,
	formatTime,
	formatNumber,
	formatPercentage,
	getCountryName,
	getContinentName,
	getDateWithOffset,
	availableRanges,
	getAvailableRanges,
	getAvailableRangesWithKeys,
	getDisplayDates,
	createValueFormatter,
	formatCurrency,
	toUnixTimestampMillis,
	formatUnixToDateTime,
	formatCurrencyCompact,
	formatDateShort,
	formatDate,
	formatDateAndTime,
	formatUnixToTime,
	formatDuration
};
