<?php

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Support\Helpers;

use DateInterval;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;

/**
 * Date/Time Helper Class
 *
 * Provides utilities for date and time manipulation including
 * formatting, comparison, and range calculations.
 *
 * @package WPUltimateSecurity\UltimateSecurity\Support\Helpers
 * @since 1.0.0
 */
class DateHelper
{
    /**
     * MySQL datetime format.
     */
    public const MYSQL_FORMAT = 'Y-m-d H:i:s';

    /**
     * ISO 8601 format.
     */
    public const ISO_FORMAT = 'c';

    /**
     * Date only format.
     */
    public const DATE_FORMAT = 'Y-m-d';

    /**
     * Time only format.
     */
    public const TIME_FORMAT = 'H:i:s';

    /**
     * Get current timestamp in GMT/UTC.
     *
     * @param string $format The date format (default: MySQL format)
     * @return string The formatted current timestamp
     */
    public static function now(string $format = self::MYSQL_FORMAT): string
    {
        return gmdate($format);
    }

    /**
     * Get current timestamp in local timezone.
     *
     * @param string $format The date format (default: MySQL format)
     * @return string The formatted current timestamp in local timezone
     */
    public static function nowLocal(string $format = self::MYSQL_FORMAT): string
    {
        return wp_date($format) ?: gmdate($format);
    }

    /**
     * Get current Unix timestamp.
     *
     * @return int The current Unix timestamp
     */
    public static function timestamp(): int
    {
        return time();
    }

    /**
     * Format a date for display using WordPress settings.
     *
     * @param string|int $date The date string or Unix timestamp
     * @param string|null $format The format string (null = WordPress date + time format)
     * @param bool $gmt Whether the input date is in GMT (default: true)
     * @return string The formatted date
     */
    public static function format(string|int $date, ?string $format = null, bool $gmt = true): string
    {
        // Convert string to timestamp if needed
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return (string) $date;
        }

        // Use WordPress date and time format if not specified
        if ($format === null) {
            $dateFormat = get_option('date_format', 'F j, Y');
            $timeFormat = get_option('time_format', 'g:i a');
            $format = $dateFormat . ' ' . $timeFormat;
        }

        if ($gmt) {
            // Convert GMT timestamp to local time
            return wp_date($format, $timestamp) ?: gmdate($format, $timestamp);
        }

        // Already in local time
        return date_i18n($format, $timestamp);
    }

    /**
     * Format a date for display with only the date portion.
     *
     * @param string|int $date The date string or Unix timestamp
     * @param bool $gmt Whether the input date is in GMT
     * @return string The formatted date
     */
    public static function formatDate(string|int $date, bool $gmt = true): string
    {
        $format = get_option('date_format', 'F j, Y');
        return self::format($date, $format, $gmt);
    }

    /**
     * Format a date for display with only the time portion.
     *
     * @param string|int $date The date string or Unix timestamp
     * @param bool $gmt Whether the input date is in GMT
     * @return string The formatted time
     */
    public static function formatTime(string|int $date, bool $gmt = true): string
    {
        $format = get_option('time_format', 'g:i a');
        return self::format($date, $format, $gmt);
    }

    /**
     * Get human-readable time difference from now.
     *
     * Examples: "5 minutes ago", "2 hours ago", "in 3 days"
     *
     * @param string|int $date The date string or Unix timestamp
     * @param string|int|null $from The reference date (default: now)
     * @return string The human-readable time difference
     */
    public static function humanDiff(string|int $date, string|int|null $from = null): string
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;
        $fromTimestamp = $from === null ? time() : (is_string($from) ? strtotime($from) : $from);

        if ($timestamp === false || $fromTimestamp === false) {
            return (string) $date;
        }

        $diff = $fromTimestamp - $timestamp;
        $isPast = $diff > 0;
        $diff = abs($diff);

        // Use WordPress's human_time_diff
        $humanDiff = human_time_diff($timestamp, $fromTimestamp);

        if ($isPast) {
            /* translators: %s: human-readable time difference */
            return sprintf(__('%s ago', 'ultimate-security'), $humanDiff);
        }

        /* translators: %s: human-readable time difference */
        return sprintf(__('in %s', 'ultimate-security'), $humanDiff);
    }

    /**
     * Get human-readable time difference (short form).
     *
     * Examples: "5m", "2h", "3d"
     *
     * @param string|int $date The date string or Unix timestamp
     * @return string The short human-readable time difference
     */
    public static function humanDiffShort(string|int $date): string
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return (string) $date;
        }

        $diff = time() - $timestamp;
        $isPast = $diff > 0;
        $diff = abs($diff);

        if ($diff < 60) {
            return ($isPast ? '' : '-') . $diff . 's';
        }

        if ($diff < 3600) {
            return ($isPast ? '' : '-') . floor($diff / 60) . 'm';
        }

        if ($diff < 86400) {
            return ($isPast ? '' : '-') . floor($diff / 3600) . 'h';
        }

        if ($diff < 604800) {
            return ($isPast ? '' : '-') . floor($diff / 86400) . 'd';
        }

        if ($diff < 2592000) {
            return ($isPast ? '' : '-') . floor($diff / 604800) . 'w';
        }

        if ($diff < 31536000) {
            return ($isPast ? '' : '-') . floor($diff / 2592000) . 'mo';
        }

        return ($isPast ? '' : '-') . floor($diff / 31536000) . 'y';
    }

    /**
     * Add an interval to a date.
     *
     * @param string|int $date The base date string or timestamp
     * @param string $interval The interval to add (e.g., '1 day', '2 hours', 'P1D')
     * @param string $format The output format (default: MySQL format)
     * @return string The resulting date
     */
    public static function add(string|int $date, string $interval, string $format = self::MYSQL_FORMAT): string
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return (string) $date;
        }

        // Try PHP DateInterval format first
        if (str_starts_with($interval, 'P')) {
            try {
                $dateTime = new DateTimeImmutable('@' . $timestamp);
                $dateInterval = new DateInterval($interval);
                $result = $dateTime->add($dateInterval);
                return $result->format($format);
            } catch (\Exception $e) {
                // Fall through to strtotime
            }
        }

        // Use strtotime for relative formats
        $newTimestamp = strtotime('+' . ltrim($interval, '+'), $timestamp);

        if ($newTimestamp === false) {
            return (string) $date;
        }

        return gmdate($format, $newTimestamp);
    }

    /**
     * Subtract an interval from a date.
     *
     * @param string|int $date The base date string or timestamp
     * @param string $interval The interval to subtract
     * @param string $format The output format (default: MySQL format)
     * @return string The resulting date
     */
    public static function subtract(string|int $date, string $interval, string $format = self::MYSQL_FORMAT): string
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return (string) $date;
        }

        // Try PHP DateInterval format first
        if (str_starts_with($interval, 'P')) {
            try {
                $dateTime = new DateTimeImmutable('@' . $timestamp);
                $dateInterval = new DateInterval($interval);
                $result = $dateTime->sub($dateInterval);
                return $result->format($format);
            } catch (\Exception $e) {
                // Fall through to strtotime
            }
        }

        // Use strtotime for relative formats
        $newTimestamp = strtotime('-' . ltrim($interval, '-'), $timestamp);

        if ($newTimestamp === false) {
            return (string) $date;
        }

        return gmdate($format, $newTimestamp);
    }

    /**
     * Check if a date has passed (is in the past).
     *
     * @param string|int $date The date to check
     * @return bool True if the date is in the past
     */
    public static function isPast(string|int $date): bool
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return false;
        }

        return $timestamp < time();
    }

    /**
     * Check if a date is in the future.
     *
     * @param string|int $date The date to check
     * @return bool True if the date is in the future
     */
    public static function isFuture(string|int $date): bool
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return false;
        }

        return $timestamp > time();
    }

    /**
     * Check if a date is today.
     *
     * @param string|int $date The date to check
     * @return bool True if the date is today
     */
    public static function isToday(string|int $date): bool
    {
        $timestamp = is_string($date) ? strtotime($date) : $date;

        if ($timestamp === false) {
            return false;
        }

        $dateStr = gmdate('Y-m-d', $timestamp);
        $todayStr = gmdate('Y-m-d');

        return $dateStr === $todayStr;
    }

    /**
     * Get a date range for common query periods.
     *
     * @param string $range The range identifier:
     *                      'today', 'yesterday', 'this_week', 'last_week',
     *                      'this_month', 'last_month', 'this_year', 'last_year',
     *                      'last_24_hours', 'last_7_days', 'last_30_days', 'last_90_days'
     * @param string $format The output format (default: MySQL format)
     * @return array{start: string, end: string} The start and end dates
     */
    public static function getRange(string $range, string $format = self::MYSQL_FORMAT): array
    {
        $now = time();
        $today = strtotime('today midnight');
        $todayEnd = strtotime('today 23:59:59');

        return match ($range) {
            'today' => [
                'start' => gmdate($format, $today),
                'end' => gmdate($format, $todayEnd),
            ],
            'yesterday' => [
                'start' => gmdate($format, strtotime('-1 day', $today)),
                'end' => gmdate($format, strtotime('-1 day', $todayEnd)),
            ],
            'this_week' => [
                'start' => gmdate($format, strtotime('monday this week', $now)),
                'end' => gmdate($format, strtotime('sunday this week 23:59:59', $now)),
            ],
            'last_week' => [
                'start' => gmdate($format, strtotime('monday last week', $now)),
                'end' => gmdate($format, strtotime('sunday last week 23:59:59', $now)),
            ],
            'this_month' => [
                'start' => gmdate($format, strtotime('first day of this month midnight', $now)),
                'end' => gmdate($format, strtotime('last day of this month 23:59:59', $now)),
            ],
            'last_month' => [
                'start' => gmdate($format, strtotime('first day of last month midnight', $now)),
                'end' => gmdate($format, strtotime('last day of last month 23:59:59', $now)),
            ],
            'this_year' => [
                'start' => gmdate($format, strtotime('first day of january this year midnight', $now)),
                'end' => gmdate($format, strtotime('last day of december this year 23:59:59', $now)),
            ],
            'last_year' => [
                'start' => gmdate($format, strtotime('first day of january last year midnight', $now)),
                'end' => gmdate($format, strtotime('last day of december last year 23:59:59', $now)),
            ],
            'last_24_hours' => [
                'start' => gmdate($format, $now - 86400),
                'end' => gmdate($format, $now),
            ],
            'last_7_days' => [
                'start' => gmdate($format, strtotime('-7 days', $today)),
                'end' => gmdate($format, $todayEnd),
            ],
            'last_30_days' => [
                'start' => gmdate($format, strtotime('-30 days', $today)),
                'end' => gmdate($format, $todayEnd),
            ],
            'last_90_days' => [
                'start' => gmdate($format, strtotime('-90 days', $today)),
                'end' => gmdate($format, $todayEnd),
            ],
            default => [
                'start' => gmdate($format, $today),
                'end' => gmdate($format, $todayEnd),
            ],
        };
    }

    /**
     * Get the difference between two dates.
     *
     * @param string|int $date1 The first date
     * @param string|int $date2 The second date
     * @param string $unit The unit to return: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'
     * @return int|float The difference in the specified unit
     */
    public static function diff(string|int $date1, string|int $date2, string $unit = 'seconds'): int|float
    {
        $timestamp1 = is_string($date1) ? strtotime($date1) : $date1;
        $timestamp2 = is_string($date2) ? strtotime($date2) : $date2;

        if ($timestamp1 === false || $timestamp2 === false) {
            return 0;
        }

        $diffSeconds = abs($timestamp2 - $timestamp1);

        return match ($unit) {
            'seconds' => $diffSeconds,
            'minutes' => $diffSeconds / 60,
            'hours' => $diffSeconds / 3600,
            'days' => $diffSeconds / 86400,
            'weeks' => $diffSeconds / 604800,
            'months' => $diffSeconds / 2592000, // Approximate 30 days
            'years' => $diffSeconds / 31536000, // Approximate 365 days
            default => $diffSeconds,
        };
    }

    /**
     * Compare two dates.
     *
     * @param string|int $date1 The first date
     * @param string|int $date2 The second date
     * @return int -1 if date1 < date2, 0 if equal, 1 if date1 > date2
     */
    public static function compare(string|int $date1, string|int $date2): int
    {
        $timestamp1 = is_string($date1) ? strtotime($date1) : $date1;
        $timestamp2 = is_string($date2) ? strtotime($date2) : $date2;

        if ($timestamp1 === false || $timestamp2 === false) {
            return 0;
        }

        return $timestamp1 <=> $timestamp2;
    }

    /**
     * Check if a date is between two other dates.
     *
     * @param string|int $date The date to check
     * @param string|int $start The start of the range
     * @param string|int $end The end of the range
     * @param bool $inclusive Whether to include the boundaries (default: true)
     * @return bool True if the date is within the range
     */
    public static function isBetween(
        string|int $date,
        string|int $start,
        string|int $end,
        bool $inclusive = true
    ): bool {
        $timestamp = is_string($date) ? strtotime($date) : $date;
        $startTimestamp = is_string($start) ? strtotime($start) : $start;
        $endTimestamp = is_string($end) ? strtotime($end) : $end;

        if ($timestamp === false || $startTimestamp === false || $endTimestamp === false) {
            return false;
        }

        if ($inclusive) {
            return $timestamp >= $startTimestamp && $timestamp <= $endTimestamp;
        }

        return $timestamp > $startTimestamp && $timestamp < $endTimestamp;
    }

    /**
     * Convert a date to a different timezone.
     *
     * @param string|int $date The date to convert
     * @param string $fromTimezone The source timezone (default: UTC)
     * @param string $toTimezone The target timezone
     * @param string $format The output format (default: MySQL format)
     * @return string The converted date
     */
    public static function convertTimezone(
        string|int $date,
        string $fromTimezone = 'UTC',
        string $toTimezone = '',
        string $format = self::MYSQL_FORMAT
    ): string {
        if (empty($toTimezone)) {
            $toTimezone = wp_timezone_string();
        }

        try {
            $timestamp = is_string($date) ? strtotime($date) : $date;
            $dateTime = new DateTime('@' . $timestamp);
            $dateTime->setTimezone(new DateTimeZone($toTimezone));

            return $dateTime->format($format);
        } catch (\Exception $e) {
            return is_string($date) ? $date : gmdate($format, $date);
        }
    }

    /**
     * Get WordPress timezone string.
     *
     * @return string The timezone string (e.g., 'America/New_York' or 'UTC+5')
     */
    public static function getTimezone(): string
    {
        return wp_timezone_string();
    }

    /**
     * Get the start of a day for a given date.
     *
     * @param string|int|null $date The date (null = today)
     * @param string $format The output format
     * @return string The start of the day
     */
    public static function startOfDay(string|int|null $date = null, string $format = self::MYSQL_FORMAT): string
    {
        if ($date === null) {
            return gmdate($format, strtotime('today midnight'));
        }

        $timestamp = is_string($date) ? strtotime($date) : $date;
        $dateStr = gmdate('Y-m-d', $timestamp);

        return gmdate($format, strtotime($dateStr . ' 00:00:00'));
    }

    /**
     * Get the end of a day for a given date.
     *
     * @param string|int|null $date The date (null = today)
     * @param string $format The output format
     * @return string The end of the day
     */
    public static function endOfDay(string|int|null $date = null, string $format = self::MYSQL_FORMAT): string
    {
        if ($date === null) {
            return gmdate($format, strtotime('today 23:59:59'));
        }

        $timestamp = is_string($date) ? strtotime($date) : $date;
        $dateStr = gmdate('Y-m-d', $timestamp);

        return gmdate($format, strtotime($dateStr . ' 23:59:59'));
    }

    /**
     * Parse a date string into a timestamp.
     *
     * @param string $date The date string to parse
     * @param string|null $format Expected format (null = auto-detect)
     * @return int|false The Unix timestamp or false on failure
     */
    public static function parse(string $date, ?string $format = null): int|false
    {
        if ($format !== null) {
            $dateTime = DateTimeImmutable::createFromFormat($format, $date);
            return $dateTime ? $dateTime->getTimestamp() : false;
        }

        return strtotime($date);
    }

    /**
     * Get the age in years from a birth date.
     *
     * @param string|int $birthDate The birth date
     * @return int The age in years
     */
    public static function age(string|int $birthDate): int
    {
        $timestamp = is_string($birthDate) ? strtotime($birthDate) : $birthDate;

        if ($timestamp === false) {
            return 0;
        }

        $birth = new DateTimeImmutable('@' . $timestamp);
        $now = new DateTimeImmutable();

        return (int) $birth->diff($now)->y;
    }

    /**
     * Check if a year is a leap year.
     *
     * @param int|null $year The year to check (null = current year)
     * @return bool True if it's a leap year
     */
    public static function isLeapYear(?int $year = null): bool
    {
        $year = $year ?? (int) gmdate('Y');

        return ($year % 4 === 0 && $year % 100 !== 0) || ($year % 400 === 0);
    }

    /**
     * Get the number of days in a month.
     *
     * @param int|null $month The month (1-12, null = current month)
     * @param int|null $year The year (null = current year)
     * @return int The number of days
     */
    public static function daysInMonth(?int $month = null, ?int $year = null): int
    {
        $month = $month ?? (int) gmdate('n');
        $year = $year ?? (int) gmdate('Y');

        return (int) gmdate('t', mktime(0, 0, 0, $month, 1, $year));
    }

    /**
     * Create a date from components.
     *
     * @param int $year The year
     * @param int $month The month (1-12)
     * @param int $day The day (1-31)
     * @param int $hour The hour (0-23)
     * @param int $minute The minute (0-59)
     * @param int $second The second (0-59)
     * @param string $format The output format
     * @return string The formatted date
     */
    public static function create(
        int $year,
        int $month = 1,
        int $day = 1,
        int $hour = 0,
        int $minute = 0,
        int $second = 0,
        string $format = self::MYSQL_FORMAT
    ): string {
        $timestamp = mktime($hour, $minute, $second, $month, $day, $year);

        return gmdate($format, $timestamp);
    }
}
