<?php

/**
 * JCOGS Image Pro - Duration Parser Service
 * =========================================
 * Natural language duration parsing with human-readable formatting
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      Duration Enhancement Implementation
 */

namespace JCOGSDesign\JCOGSImagePro\Service;

class DurationParser
{
    /**
     * Special duration keywords
     */
    const SPECIAL_VALUES = [
        'forever' => -1,
        'never expire' => -1,
        'permanent' => -1,
        'perpetual' => -1,
        'never' => 0,
        'disabled' => 0,
        'no cache' => 0,
        'no caching' => 0,
        'off' => 0,
        'daily' => 86400,
        'weekly' => 604800,
        'monthly' => 2592000
    ];

    /**
     * Parse natural language duration input to seconds
     * 
     * @param string $input Natural language input
     * @return array{value: int, error: string|null, parsed_from: string}
     */
    public function parseToSeconds(string $input): array
    {
        $input = trim(strtolower($input));
        
        if (empty($input)) {
            return ['value' => 0, 'error' => 'Duration cannot be empty', 'parsed_from' => $input];
        }

        // Check for special keywords first
        foreach (self::SPECIAL_VALUES as $keyword => $value) {
            if ($input === $keyword) {
                return ['value' => $value, 'error' => null, 'parsed_from' => $input];
            }
        }

        // If it's already a number, validate and return
        if (is_numeric($input)) {
            $seconds = (int)$input;
            if ($seconds < -1) {
                return ['value' => 0, 'error' => 'Duration must be -1 (permanent), 0 (disabled), or positive seconds', 'parsed_from' => $input];
            }
            return ['value' => $seconds, 'error' => null, 'parsed_from' => $input];
        }

        try {
            $seconds = $this->parseNaturalLanguage($input);
            return ['value' => $seconds, 'error' => null, 'parsed_from' => $input];
        } catch (\Exception $e) {
            return ['value' => 0, 'error' => $this->generateParsingError($input, $e->getMessage()), 'parsed_from' => $input];
        }
    }

    /**
     * Parse natural language duration input.
     */
    private function parseNaturalLanguage(string $input): int
    {
        $input = trim($input);

        // Support ISO 8601 durations such as PT1H30M without Carbon.
        if (preg_match('/^p/i', $input)) {
            return $this->parseIso8601Duration($input);
        }

        // Pre-process number words to digits using wordsToNumber
        require_once __DIR__ . '/wordsToNumber.php';
        $original = $input;
        $converted = wordsToNumber($input);
        // Replace number words with their numeric equivalents
        if ($original !== $converted) {
            // Find all number words in the original string
            $numwords = [
                'zero','one','two','three','four','five','six','seven','eight','nine','ten',
                'eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen','twenty',
                'thirty','forty','fifty','sixty','seventy','eighty','ninety','hundred','thousand','million','billion'
            ];
            foreach ($numwords as $word) {
                $pattern = '/\b' . $word . '\b/';
                if (preg_match($pattern, $original)) {
                    // Find the corresponding number in the converted string
                    if (preg_match('/\b(\d+)\b/', $converted, $matches)) {
                        $number = $matches[1];
                        $original = preg_replace($pattern, $number, $original, 1);
                        // Remove the matched number from converted to avoid duplicate replacements
                        $converted = preg_replace('/\b' . $number . '\b/', '', $converted, 1);
                    }
                }
            }
            $input = $original;
        }

        try {
            return $this->parseWithCustomNormalization($input);
        } catch (\InvalidArgumentException $e) {
            // Fall back to relative date phrases like "tomorrow" or "next week".
            return $this->parseRelativeDateExpression($input);
        }
    }

    /**
     * Custom parsing with normalization for flexible natural language input.
     */
    private function parseWithCustomNormalization(string $input): int
    {
        $input = strtolower(trim($input));

        // Strip common separators/prefixes that are not semantically relevant.
        $input = preg_replace('/^in\s+/', '', $input);
        $input = str_replace([',', ';'], ' ', $input);

        // Normalize common phrases before regex parsing.
        $input = str_replace([
            'a couple of ', 'couple of ', 'a couple ', 'couple ',
            'a few ', 'few ',
            'a ', 'an ', 'one ',
            'half a ', 'half an ',
            'every ', 'each ',
            'and a half'
        ], [
            '2 ', '2 ', '2 ', '2 ',
            '3 ', '3 ',
            '1 ', '1 ', '1 ',
            '0.5 ', '0.5 ',
            '1 ', '1 ',
            '.5'
        ], $input);

        // Normalize common unit abbreviations.
        $input = preg_replace('/\bsecs?\b/', 'seconds', $input);
        $input = preg_replace('/\bmins?\b/', 'minutes', $input);
        $input = preg_replace('/\bhrs?\b/', 'hours', $input);
        $input = preg_replace('/\bmos?\b/', 'months', $input);
        $input = preg_replace('/\byrs?\b/', 'years', $input);

        // Handle fractional expressions
        $input = preg_replace('/(\d+)\.5\s+(minute|hour|day|week|month|year)s?/', '$1.5 $2s', $input);

        // Normalize repeated whitespace.
        $input = preg_replace('/\s+/', ' ', trim($input));

        // Parse normalized input via regex.
        return $this->parseWithRegex($input);
    }

    /**
    * Regex-based parsing for natural language durations.
     */
    private function parseWithRegex(string $input): int
    {
        // Handle simple single unit durations
        if (preg_match('/^(\d+(?:\.\d+)?)\s*(second|minute|hour|day|week|month|year)s?$/', $input, $matches)) {
            $amount = floatval($matches[1]);
            $unit = $matches[2];
            
            return $this->convertToSeconds($amount, $unit);
        }

        // Handle compound durations like "1 week 2 days"
        if (preg_match_all('/(\d+(?:\.\d+)?)\s*(second|minute|hour|day|week|month|year)s?/', $input, $matches, PREG_SET_ORDER)) {
            $totalSeconds = 0;
            foreach ($matches as $match) {
                $amount = floatval($match[1]);
                $unit = $match[2];
                $totalSeconds += $this->convertToSeconds($amount, $unit);
            }
            return $totalSeconds;
        }

        throw new \InvalidArgumentException("Could not parse duration: {$input}");
    }

    /**
     * Convert amount and unit to seconds
     */
    private function convertToSeconds(float $amount, string $unit): int
    {
        $multipliers = [
            'second' => 1,
            'minute' => 60,
            'hour' => 3600,
            'day' => 86400,
            'week' => 604800,
            'month' => 2592000, // 30 days
            'year' => 31536000  // 365 days
        ];

        if (!isset($multipliers[$unit])) {
            throw new \InvalidArgumentException("Unknown time unit: {$unit}");
        }

        return (int)round($amount * $multipliers[$unit]);
    }

    /**
     * Parse ISO 8601 duration strings using native DateInterval.
     */
    private function parseIso8601Duration(string $input): int
    {
        try {
            $interval = new \DateInterval(strtoupper($input));

            // DateInterval months/years are approximated to match existing parser conventions.
            $seconds = 0;
            $seconds += (int)$interval->y * 31536000;
            $seconds += (int)$interval->m * 2592000;
            $seconds += (int)$interval->d * 86400;
            $seconds += (int)$interval->h * 3600;
            $seconds += (int)$interval->i * 60;
            $seconds += (int)$interval->s;

            if ($interval->invert === 1) {
                $seconds *= -1;
            }

            return $seconds;
        } catch (\Exception $e) {
            throw new \InvalidArgumentException('Invalid ISO 8601 duration: ' . $input);
        }
    }

    /**
     * Parse relative date expressions (for example "tomorrow", "next week").
     */
    private function parseRelativeDateExpression(string $input): int
    {
        $now = time();
        $target = strtotime($input, $now);

        if ($target === false) {
            throw new \InvalidArgumentException('Could not parse relative date expression: ' . $input);
        }

        $seconds = (int)($target - $now);

        // For duration fields, require a forward duration.
        if ($seconds <= 0) {
            throw new \InvalidArgumentException('Relative date expression must resolve to a future time: ' . $input);
        }

        return $seconds;
    }

    /**
    * Format seconds to a human-readable duration string.
     * 
     * @param int $seconds Duration in seconds
     * @param bool $abbreviated Use abbreviated format (e.g., "2h" vs "2 hours")
     * @return string Human-readable duration
     */
    public function formatDuration(int $seconds, bool $abbreviated = false): string
    {
        // Handle special values
        if ($seconds === -1) {
            return $abbreviated ? 'forever' : 'forever (never expires)';
        }
        
        if ($seconds === 0) {
            return $abbreviated ? 'disabled' : 'disabled (no caching)';
        }

        if ($seconds < 0) {
            return $abbreviated ? 'invalid' : 'invalid duration';
        }

        // For very small values (< 2 minutes), show in seconds
        if ($seconds < 120) {
            $unit = $abbreviated ? 's' : ($seconds == 1 ? ' second' : ' seconds');
            return $seconds . $unit;
        }

        return $this->formatBasic($seconds, $abbreviated);
    }

    /**
     * Basic fallback formatting when Carbon is not available
     */
    private function formatBasic(int $seconds, bool $abbreviated): string
    {
        // For small values (< 2 hours), show in minutes
        if ($seconds < 7200) {
            $minutes = round($seconds / 60);
            $unit = $abbreviated ? 'm' : ($minutes == 1 ? ' minute' : ' minutes');
            return $minutes . $unit;
        }

        // For medium values (< 2 days), show in hours
        if ($seconds < 172800) {
            $hours = round($seconds / 3600, 1);
            if ($hours == floor($hours)) {
                $hours = (int)$hours;
            }
            $unit = $abbreviated ? 'h' : ($hours == 1 ? ' hour' : ' hours');
            return $hours . $unit;
        }

        // For large values (< 2 months), show in days
        if ($seconds < 5184000) {
            $days = round($seconds / 86400);
            $unit = $abbreviated ? 'd' : ($days == 1 ? ' day' : ' days');
            return $days . $unit;
        }

        // For very large values, show in months/years
        if ($seconds < 31536000) {
            $months = round($seconds / 2592000, 1);
            if ($months == floor($months)) {
                $months = (int)$months;
            }
            $unit = $abbreviated ? 'mo' : ($months == 1 ? ' month' : ' months');
            return "about " . $months . $unit;
        }

        // Show in years
        $years = round($seconds / 31536000, 1);
        if ($years == floor($years)) {
            $years = (int)$years;
        }
        $unit = $abbreviated ? 'y' : ($years == 1 ? ' year' : ' years');
        return "about " . $years . $unit;
    }

    /**
     * Generate helpful parsing error message.
     */
    private function generateParsingError(string $input, string $parseError): string
    {
        return "Could not parse '{$input}'. Try formats like: '2 weeks', '1 hour 30 minutes', 'a week', 'half an hour', 'tomorrow', 'next week', 'PT1H30M' (ISO 8601), '604800' (seconds), 'forever', or 'disabled'.";
    }

    /**
     * Get duration examples for UI help text
     * 
     * @param string $context Context for appropriate examples (cache, timeout, audit)
     * @return array Array of example strings
     */
    public function getExamples(string $context = 'general'): array
    {
        $common = [
            '30 seconds',
            'a minute',
            '2 hours',
            'a day',
            'a week',
            '1 hour 30 minutes',
            'forever',
            'disabled'
        ];

        $contextExamples = [
            'cache' => [
                'a day',
                'a week',
                '2 weeks',
                'a month',
                '1 week 3 days',
                'forever',
                'disabled'
            ],
            'audit' => [
                'daily',
                'a couple days',
                'a week',
                '2 weeks'
            ],
            'timeout' => [
                '30 seconds',
                'a minute',
                'a couple minutes',
                '5 minutes'
            ]
        ];

        return $contextExamples[$context] ?? $common;
    }

    /**
     * Validate duration for specific context
     * 
     * @param int $seconds Duration in seconds
     * @param string $context Context for validation (cache, timeout, audit)
     * @return array{valid: bool, error: string|null}
     */
    public function validateForContext(int $seconds, string $context): array
    {
        switch ($context) {
            case 'cache':
                if ($seconds < -1) {
                    return ['valid' => false, 'error' => 'Cache duration must be -1 (permanent), 0 (disabled), or positive seconds'];
                }
                return ['valid' => true, 'error' => null];
                
            case 'timeout':
                if ($seconds <= 0) {
                    return ['valid' => false, 'error' => 'Timeout must be greater than 0 seconds'];
                }
                if ($seconds > 3600) {
                    return ['valid' => false, 'error' => 'Timeout should not exceed 1 hour (3600 seconds)'];
                }
                return ['valid' => true, 'error' => null];
                
            case 'audit':
                if ($seconds <= 0) {
                    return ['valid' => false, 'error' => 'Audit interval must be greater than 0 seconds'];
                }
                if ($seconds < 3600) {
                    return ['valid' => false, 'error' => 'Audit interval should be at least 1 hour for performance reasons'];
                }
                return ['valid' => true, 'error' => null];
                
            default:
                if ($seconds < -1) {
                    return ['valid' => false, 'error' => 'Duration must be -1, 0, or positive seconds'];
                }
                return ['valid' => true, 'error' => null];
        }
    }
}
