<?php

/**
 * Download Manager Service
 *
 * Provides downloadable files based on subscription rules.
 * Works alongside WooCommerce native downloadable products (does not interfere with them).
 *
 * @package ArraySubs\Features\MembersAccess\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\MembersAccess\Services;

defined('ABSPATH') || exit;

/**
 * DownloadManager class
 *
 * Manages download rules that provide files to customers based on subscription conditions.
 * These files appear on the My Account → Downloads page alongside WooCommerce native downloads.
 */
class DownloadManager
{
    /**
     * User meta key for download usage tracking
     *
     * @var string
     */
    private const DOWNLOAD_USAGE_META_KEY = '_arraysubs_download_usage';

    /**
     * Constructor
     */
    public function __construct()
    {
        // Add subscription-based downloads to WooCommerce downloads
        add_filter('woocommerce_customer_get_downloadable_products', [$this, 'addSubscriptionDownloads'], 20, 1);

        // Handle download requests for subscription files
        add_action('template_redirect', [$this, 'handleDownloadRequest']);
    }

    /**
     * Add subscription-based downloads to customer's download list
     *
     * This method appends files from matching download rules to the customer's
     * My Account → Downloads page. It does NOT interfere with WooCommerce native downloads.
     *
     * @param array $downloads Existing WooCommerce downloads.
     * @return array Modified downloads array.
     */
    public function addSubscriptionDownloads(array $downloads): array
    {
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return $downloads;
        }

        // Get download rules
        $rules = arraysubs_get_setting('members_access.download_rules', []);
        if (empty($rules)) {
            return $downloads;
        }

        $evaluator = new ConditionEvaluator();

        foreach ($rules as $rule_index => $rule) {
            // Skip disabled rules
            if (empty($rule['enabled'])) {
                continue;
            }

            // Get conditions
            $conditions = $rule['conditions'] ?? ['logic' => 'and', 'rules' => []];

            // Check if user meets conditions
            if (!$evaluator->evaluate($user_id, $conditions)) {
                continue;
            }

            // Check dripping schedule
            $schedule_evaluator = new ScheduleEvaluator();
            if (!$schedule_evaluator->isScheduleSatisfied($user_id, $rule)) {
                continue;
            }

            // User meets conditions - add files to downloads
            $files = $rule['files'] ?? [];
            foreach ($files as $file_index => $file) {
                if (empty($file['url'])) {
                    continue;
                }

                // Generate a unique download ID for this rule+file combination
                $download_id = 'arraysubs_' . $rule_index . '_' . $file_index;

                $downloads[] = [
                    'download_id'       => $download_id,
                    'download_url'      => $this->getSecureDownloadUrl($download_id, $file['url']),
                    'download_name'     => $file['name'] ?: basename($file['url']),
                    'product_id'        => 0,
                    'product_name'      => $rule['name'] ?? __('Subscription Download', 'arraysubs'),
                    'product_url'       => '',
                    'order_id'          => 0,
                    'order_key'         => '',
                    'downloads_remaining' => '',
                    'access_expires'    => null,
                    'file'              => [
                        'file' => $file['url'],
                        'name' => $file['name'] ?: basename($file['url']),
                    ],
                ];
            }
        }

        return $downloads;
    }

    /**
     * Generate secure download URL
     *
     * @param string $download_id Unique download ID.
     * @param string $file_url    File URL.
     * @return string Secure download URL.
     */
    private function getSecureDownloadUrl(string $download_id, string $file_url): string
    {
        // Generate a signed URL for security
        $nonce = wp_create_nonce('arraysubs_download_' . $download_id);

        return add_query_arg(
            [
                'arraysubs_download' => $download_id,
                'nonce'              => $nonce,
            ],
            home_url('/')
        );
    }

    /**
     * Handle download requests for subscription files
     *
     * @return void
     */
    public function handleDownloadRequest(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (empty($_GET['arraysubs_download'])) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $download_id = sanitize_text_field(wp_unslash($_GET['arraysubs_download']));
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $nonce = isset($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : '';

        // Verify nonce
        if (!wp_verify_nonce($nonce, 'arraysubs_download_' . $download_id)) {
            wp_die(
                esc_html__('Invalid download link or link has expired.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 403]
            );
        }

        // Must be logged in
        if (!is_user_logged_in()) {
            wp_die(
                esc_html__('You must be logged in to download this file.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 403]
            );
        }

        $user_id = get_current_user_id();

        // Parse download ID - expected format: arraysubs followed by rule index and file index
        $parts = explode('_', $download_id);
        if (count($parts) < 3 || $parts[0] !== 'arraysubs') {
            wp_die(
                esc_html__('Invalid download identifier.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 400]
            );
        }

        $rule_index = (int) $parts[1];
        $file_index = (int) $parts[2];

        // Get download rules
        $rules = arraysubs_get_setting('members_access.download_rules', []);
        if (!isset($rules[$rule_index])) {
            wp_die(
                esc_html__('Download not found.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 404]
            );
        }

        $rule = $rules[$rule_index];

        // Check if rule is enabled
        if (empty($rule['enabled'])) {
            wp_die(
                esc_html__('This download is currently unavailable.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 403]
            );
        }

        // Check if downloads are allowed for this rule
        if (isset($rule['allow_download']) && !$this->toBool($rule['allow_download'])) {
            wp_die(
                esc_html__('Downloads are not allowed for this rule.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 403]
            );
        }

        // Verify user still meets conditions
        $evaluator = new ConditionEvaluator();
        $conditions = $rule['conditions'] ?? ['logic' => 'and', 'rules' => []];

        if (!$evaluator->evaluate($user_id, $conditions)) {
            wp_die(
                esc_html__('You no longer have access to this download.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 403]
            );
        }

        // Check dripping schedule
        $schedule_evaluator = new ScheduleEvaluator();
        if (!$schedule_evaluator->isScheduleSatisfied($user_id, $rule)) {
            wp_die(
                esc_html__('This download is not yet available. Please check back later.', 'arraysubs'),
                esc_html__('Download Not Available', 'arraysubs'),
                ['response' => 403]
            );
        }

        // Enforce download limit if configured
        $limit = (int) ($rule['download_limit'] ?? 0);
        $period = isset($rule['limit_period']) ? sanitize_key((string) $rule['limit_period']) : 'month';
        if ($limit > 0) {
            $used = $this->getDownloadCount($user_id, $period);
            if ($used >= $limit) {
                wp_die(
                    esc_html__('You have reached your download limit for this period.', 'arraysubs'),
                    esc_html__('Download Limit Reached', 'arraysubs'),
                    ['response' => 403]
                );
            }
        }

        // Get file
        $files = $rule['files'] ?? [];
        if (!isset($files[$file_index]) || empty($files[$file_index]['url'])) {
            wp_die(
                esc_html__('File not found.', 'arraysubs'),
                esc_html__('Download Error', 'arraysubs'),
                ['response' => 404]
            );
        }

        $file = $files[$file_index];
        $file_url = $file['url'];
        $file_name = $file['name'] ?: basename($file_url);

        // Record usage before serving file
        if ($limit > 0) {
            $this->incrementDownloadCount($user_id, $period);
        }

        // Serve the file
        $this->serveFile($file_url, $file_name);
    }

    /**
     * Serve file for download
     *
     * @param string $file_url  File URL or path.
     * @param string $file_name Display name for the file.
     * @return void
     */
    private function serveFile(string $file_url, string $file_name): void
    {
        // Convert URL to local path if it's a local file
        $upload_dir = wp_upload_dir();
        $file_path = '';

        if (strpos($file_url, $upload_dir['baseurl']) !== false) {
            // It's a local upload - convert URL to path
            $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_url);
        } elseif (strpos($file_url, site_url()) !== false) {
            // It's somewhere else on the site
            $file_path = str_replace(site_url('/'), ABSPATH, $file_url);
        }

        // If we have a local file, serve it directly
        if ($file_path && file_exists($file_path)) {
            $this->downloadLocalFile($file_path, $file_name);
            exit;
        }

        // Otherwise redirect to the file URL
        // (for external files or CDN-hosted files)
        wp_safe_redirect($file_url);
        exit;
    }

    /**
     * Download a local file
     *
     * @param string $file_path Local file path.
     * @param string $file_name Display name for the file.
     * @return void
     */
    private function downloadLocalFile(string $file_path, string $file_name): void
    {
        // Get file info
        $mime_type = wp_check_filetype($file_path);
        $content_type = $mime_type['type'] ?: 'application/octet-stream';
        $file_size = filesize($file_path);

        // Send headers
        nocache_headers();
        header('Content-Type: ' . $content_type);
        header('Content-Disposition: attachment; filename="' . sanitize_file_name($file_name) . '"');
        header('Content-Length: ' . $file_size);
        header('Content-Transfer-Encoding: binary');
        header('X-Content-Type-Options: nosniff');

        // Clean output buffer
        if (ob_get_level()) {
            ob_end_clean();
        }

        // Serve file
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile($file_path);
        exit;
    }

    /**
     * Get downloads available for a specific user
     *
     * @param int $user_id User ID.
     * @return array Array of available downloads.
     */
    public function getAvailableDownloads(int $user_id): array
    {
        if ($user_id <= 0) {
            return [];
        }

        $available = [];
        $rules = arraysubs_get_setting('members_access.download_rules', []);

        if (empty($rules)) {
            return $available;
        }

        $evaluator = new ConditionEvaluator();
        $schedule_evaluator = new ScheduleEvaluator();

        foreach ($rules as $rule_index => $rule) {
            if (empty($rule['enabled'])) {
                continue;
            }

            $conditions = $rule['conditions'] ?? ['logic' => 'and', 'rules' => []];

            if (!$evaluator->evaluate($user_id, $conditions)) {
                continue;
            }

            if (!$schedule_evaluator->isScheduleSatisfied($user_id, $rule)) {
                continue;
            }

            $files = $rule['files'] ?? [];
            foreach ($files as $file_index => $file) {
                if (empty($file['url'])) {
                    continue;
                }

                $download_id = 'arraysubs_' . $rule_index . '_' . $file_index;

                $available[] = [
                    'download_id'  => $download_id,
                    'rule_name'    => $rule['name'] ?? '',
                    'file_name'    => $file['name'] ?: basename($file['url']),
                    'file_url'     => $file['url'],
                    'download_url' => $this->getSecureDownloadUrl($download_id, $file['url']),
                ];
            }
        }

        return $available;
    }

    /**
     * Get current download count for user and period
     *
     * @param int    $user_id User ID.
     * @param string $period  Period key: day, week, month, year, lifetime.
     * @return int
     */
    public function getDownloadCount(int $user_id, string $period = 'month'): int
    {
        if ($user_id <= 0) {
            return 0;
        }

        $period = $this->sanitizePeriod($period);
        $usage = $this->getUsageData($user_id);

        if (!isset($usage[$period]) || !is_array($usage[$period])) {
            return 0;
        }

        if ($period !== 'lifetime') {
            $current_period_start = $this->getPeriodStartTimestamp($period);
            $stored_period_start = isset($usage[$period]['period_start']) ? absint($usage[$period]['period_start']) : 0;

            // Auto-reset when period changes
            if ($stored_period_start !== $current_period_start) {
                $usage[$period] = [
                    'count'        => 0,
                    'period_start' => $current_period_start,
                ];

                $this->saveUsageData($user_id, $usage);

                return 0;
            }
        }

        return isset($usage[$period]['count']) ? absint($usage[$period]['count']) : 0;
    }

    /**
     * Check download limit for user and product
     *
     * @param int $user_id    User ID.
     * @param int $product_id Product ID.
     * @return array{allowed:bool,remaining:int,limit:int,used:int,period:string}
     */
    public function checkDownloadLimit(int $user_id, int $product_id): array
    {
        $result = [
            'allowed'   => true,
            'remaining' => -1,
            'limit'     => 0,
            'used'      => 0,
            'period'    => 'month',
        ];

        if ($user_id <= 0 || $product_id <= 0) {
            $result['allowed'] = false;
            $result['remaining'] = 0;

            return $result;
        }

        $rules = arraysubs_get_setting('members_access.download_rules', []);
        if (empty($rules)) {
            return $result;
        }

        $evaluator = new ConditionEvaluator();
        $schedule_evaluator = new ScheduleEvaluator();
        $best_remaining = PHP_INT_MAX;

        foreach ($rules as $rule) {
            if (empty($rule['enabled'])) {
                continue;
            }

            $target = isset($rule['target']) ? sanitize_key((string) $rule['target']) : 'all';
            if ($target === 'specific_products') {
                $product_ids = isset($rule['product_ids']) && is_array($rule['product_ids']) ? array_map('absint', $rule['product_ids']) : [];
                if (!in_array($product_id, $product_ids, true)) {
                    continue;
                }
            }

            $conditions = $rule['conditions'] ?? ['logic' => 'and', 'rules' => []];
            if (!$evaluator->evaluate($user_id, $conditions)) {
                continue;
            }

            if (!$schedule_evaluator->isScheduleSatisfied($user_id, $rule)) {
                continue;
            }

            if (isset($rule['allow_download']) && !$this->toBool($rule['allow_download'])) {
                $result['allowed'] = false;
                $result['remaining'] = 0;

                return $result;
            }

            $limit = (int) ($rule['download_limit'] ?? 0);
            if ($limit <= 0) {
                continue;
            }

            $period = isset($rule['limit_period']) ? $this->sanitizePeriod((string) $rule['limit_period']) : 'month';
            $used = $this->getDownloadCount($user_id, $period);
            $remaining = max(0, $limit - $used);

            if ($remaining < $best_remaining) {
                $best_remaining = $remaining;
                $result['limit'] = $limit;
                $result['used'] = $used;
                $result['remaining'] = $remaining;
                $result['period'] = $period;
                $result['allowed'] = $remaining > 0;
            }

            if (!$result['allowed']) {
                return $result;
            }
        }

        return $result;
    }

    /**
     * Handle renewal complete event and reset eligible download counters
     *
     * @param int $subscription_id Subscription ID.
     * @return void
     */
    public function onRenewalComplete(int $subscription_id): void
    {
        if ($subscription_id <= 0) {
            return;
        }

        $user_id = absint(get_post_meta($subscription_id, '_customer_id', true));
        if ($user_id <= 0) {
            $user_id = absint(get_post_meta($subscription_id, '_customer_user', true));
        }

        if ($user_id <= 0) {
            return;
        }

        $rules = arraysubs_get_setting('members_access.download_rules', []);
        if (empty($rules)) {
            return;
        }

        $periods_to_reset = [];

        foreach ($rules as $rule) {
            if (empty($rule['enabled']) || !$this->toBool($rule['reset_on_renewal'] ?? false)) {
                continue;
            }

            $limit = (int) ($rule['download_limit'] ?? 0);
            if ($limit <= 0) {
                continue;
            }

            $periods_to_reset[] = isset($rule['limit_period'])
                ? $this->sanitizePeriod((string) $rule['limit_period'])
                : 'month';
        }

        if (empty($periods_to_reset)) {
            return;
        }

        foreach (array_unique($periods_to_reset) as $period) {
            $this->resetDownloadCount($user_id, $period);
        }
    }

    /**
     * Increment user's download count for period
     *
     * @param int    $user_id User ID.
     * @param string $period  Period key.
     * @return void
     */
    private function incrementDownloadCount(int $user_id, string $period): void
    {
        if ($user_id <= 0) {
            return;
        }

        $period = $this->sanitizePeriod($period);
        $usage = $this->getUsageData($user_id);
        $current_count = $this->getDownloadCount($user_id, $period);

        $usage[$period] = [
            'count'        => $current_count + 1,
            'period_start' => $period === 'lifetime' ? 0 : $this->getPeriodStartTimestamp($period),
        ];

        $this->saveUsageData($user_id, $usage);
    }

    /**
     * Reset user's download count
     *
     * @param int         $user_id User ID.
     * @param string|null $period  Optional period key. If null, resets all periods.
     * @return void
     */
    private function resetDownloadCount(int $user_id, ?string $period = null): void
    {
        if ($user_id <= 0) {
            return;
        }

        if ($period === null) {
            delete_user_meta($user_id, self::DOWNLOAD_USAGE_META_KEY);
            return;
        }

        $period = $this->sanitizePeriod($period);
        $usage = $this->getUsageData($user_id);

        $usage[$period] = [
            'count'        => 0,
            'period_start' => $period === 'lifetime' ? 0 : $this->getPeriodStartTimestamp($period),
        ];

        $this->saveUsageData($user_id, $usage);
    }

    /**
     * Get usage data from user meta
     *
     * @param int $user_id User ID.
     * @return array
     */
    private function getUsageData(int $user_id): array
    {
        $usage = get_user_meta($user_id, self::DOWNLOAD_USAGE_META_KEY, true);
        return is_array($usage) ? $usage : [];
    }

    /**
     * Save usage data to user meta
     *
     * @param int   $user_id User ID.
     * @param array $usage   Usage data.
     * @return void
     */
    private function saveUsageData(int $user_id, array $usage): void
    {
        update_user_meta($user_id, self::DOWNLOAD_USAGE_META_KEY, $usage);
    }

    /**
     * Sanitize period key
     *
     * @param string $period Period key.
     * @return string
     */
    private function sanitizePeriod(string $period): string
    {
        $period = sanitize_key($period);
        $allowed = ['day', 'week', 'month', 'year', 'lifetime'];

        return in_array($period, $allowed, true) ? $period : 'month';
    }

    /**
     * Normalize mixed values to boolean
     *
     * @param mixed $value Value to normalize.
     * @return bool
     */
    private function toBool($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_string($value)) {
            $normalized = strtolower(trim($value));
            if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) {
                return true;
            }

            if (in_array($normalized, ['0', 'false', 'no', 'off', ''], true)) {
                return false;
            }
        }

        return !empty($value);
    }

    /**
     * Get timestamp representing period start in site timezone
     *
     * @param string $period Period key.
     * @return int
     */
    private function getPeriodStartTimestamp(string $period): int
    {
        $now = new \DateTimeImmutable('now', wp_timezone());

        switch ($period) {
            case 'day':
                return (int) $now->setTime(0, 0)->getTimestamp();

            case 'week':
                $day_of_week = (int) $now->format('N');
                return (int) $now->modify('-' . ($day_of_week - 1) . ' days')->setTime(0, 0)->getTimestamp();

            case 'month':
                return (int) $now->modify('first day of this month')->setTime(0, 0)->getTimestamp();

            case 'year':
                return (int) $now->modify('first day of january this year')->setTime(0, 0)->getTimestamp();

            case 'lifetime':
            default:
                return 0;
        }
    }
}
