<?php
declare(strict_types=1);

namespace Instinctor\Services;

use Instinctor\Core\Config;
use Instinctor\Core\DB;
use Instinctor\Core\Storage;
use Instinctor\Core\Session;

if (!defined('ABSPATH')) exit;

/**
 * Digital Download Service
 * Manages downloadable files, purchase permissions, and secure delivery.
 */
class DigitalDownloadService
{
    private static ?self $instance = null;

    public static function instance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct()
    {
        // Secure download endpoint
        $this->registerAjax('instinctor_download_file', [$this, 'serveDownload']);
        add_action('wp_ajax_nopriv_instinctor_download_file', [$this, 'serveDownload']);

        // Grant permissions when order completes
        add_action('instinctor_order_completed', [$this, 'grantPermissionsForOrder']);

        // Also grant on payment (Stripe webhook may mark paid before completed)
        add_action('instinctor_order_paid', [$this, 'grantPermissionsForOrder']);

        // Subscription checkout path: CheckoutService::finalize() fires
        // instinctor_order_finalized after the order is fully processed.
        // That's the universal "paid + finalized" signal — covers initial
        // subscription orders AND every renewal order. The duplicate-grant
        // guard inside grantPermissionsForOrder() prevents double-grants
        // when both _paid and _finalized fire for the same order.
        add_action('instinctor_order_finalized', [$this, 'grantPermissionsForOrder']);
    }

    // =========================================================================
    // FILE MANAGEMENT
    // =========================================================================

    /**
     * Get all download files for a product
     */
    public function getProductDownloads(int $productId): array
    {
        $p = DB::prefix();
        return DB::get_results(
            DB::prepare(
                "SELECT * FROM {$p}instinctor_downloads WHERE product_id = %d ORDER BY sort_order ASC, id ASC",
                $productId
            ),
            ARRAY_A
        ) ?: [];
    }

    /**
     * Save (add or update) a download file for a product
     */
    public function saveDownload(int $productId, array $data): int|false
    {

        $id       = (int) ($data['id'] ?? 0);
        $name     = sanitize_text_field($data['name'] ?? '');
        $fileUrl  = esc_url_raw($data['file_url'] ?? '');
        $limit    = isset($data['download_limit']) && $data['download_limit'] !== '' ? max(0, (int)$data['download_limit']) : null;
        $expiry   = isset($data['expiry_days']) && $data['expiry_days'] !== '' ? max(0, (int)$data['expiry_days']) : null;
        $sort     = (int) ($data['sort_order'] ?? 0);

        if (!$name || !$fileUrl) return false;

        $fileType = wp_check_filetype(basename((string) wp_parse_url($fileUrl, PHP_URL_PATH)))['type'] ?: null;

        if ($id > 0) {
            DB::update(
                DB::prefix() . 'instinctor_downloads',
                compact('name', 'fileUrl', 'fileType', 'limit', 'expiry', 'sort') + ['file_url' => $fileUrl, 'file_type' => $fileType, 'download_limit' => $limit, 'expiry_days' => $expiry, 'sort_order' => $sort],
                ['id' => $id, 'product_id' => $productId]
            );
            return $id;
        }

        $result = DB::insert(
            DB::prefix() . 'instinctor_downloads',
            [
                'product_id'     => $productId,
                'name'           => $name,
                'file_url'       => $fileUrl,
                'file_type'      => $fileType,
                'download_limit' => $limit,
                'expiry_days'    => $expiry,
                'sort_order'     => $sort,
            ]
        );
        return $result ? DB::insert_id() : false;
    }

    /**
     * Delete a download file (and all permissions for it)
     */
    public function deleteDownload(int $downloadId, int $productId): bool
    {
        DB::delete(DB::prefix() . 'instinctor_download_permissions', ['download_id' => $downloadId]);
        return (bool) DB::delete(
            DB::prefix() . 'instinctor_downloads',
            ['id' => $downloadId, 'product_id' => $productId]
        );
    }

    // =========================================================================
    // PERMISSION GRANTING
    // =========================================================================

    /**
     * Grant download permissions for all downloadable products in a completed order
     */
    public function grantPermissionsForOrder(int $orderId): void
    {
        $p = DB::prefix();

        // Avoid duplicate grants
        $alreadyGranted = DB::get_var(
            DB::prepare(
                "SELECT COUNT(*) FROM {$p}instinctor_download_permissions WHERE order_id = %d",
                $orderId
            )
        );
        if ($alreadyGranted > 0) return;

        // Get order customer + items
        $order = DB::get_row(
            DB::prepare(
                "SELECT o.*, c.email, c.first_name FROM {$p}instinctor_orders o LEFT JOIN {$p}instinctor_customers c ON c.id = o.customer_id WHERE o.id = %d",
                $orderId
            )
        );
        if (!$order || !$order->customer_id) return;

        $items = DB::get_results(
            DB::prepare(
                "SELECT product_id FROM {$p}instinctor_order_items WHERE order_id = %d",
                $orderId
            )
        );
        if (empty($items)) return;

        $granted = [];
        foreach ($items as $item) {
            $isDownloadable = DB::get_var(
                DB::prepare(
                    "SELECT downloadable FROM {$p}instinctor_products WHERE id = %d",
                    $item->product_id
                )
            );
            if (!$isDownloadable) continue;

            $downloads = $this->getProductDownloads((int) $item->product_id);
            foreach ($downloads as $dl) {
                $expiry = null;
                if (!empty($dl['expiry_days'])) {
                    $expiry = gmdate('Y-m-d H:i:s', strtotime('+' . (int)$dl['expiry_days'] . ' days'));
                }
                DB::insert(
                    DB::prefix() . 'instinctor_download_permissions',
                    [
                        'download_id'    => (int) $dl['id'],
                        'order_id'       => $orderId,
                        'customer_id'    => (int) $order->customer_id,
                        'download_count' => 0,
                        'access_granted' => current_time('mysql'),
                        'access_expires' => $expiry,
                    ]
                );
                $permId   = DB::insert_id();
                $granted[] = [
                    'name'          => $dl['name'],
                    'download_url'  => $this->generateDownloadUrl($permId, (int)$order->customer_id),
                    'expiry'        => $expiry ? date_i18n(Config::get('date_format', 'Y-m-d'), strtotime($expiry)) : null,
                    'limit'         => $dl['download_limit'] ?: null,
                ];
            }
        }

        // Send delivery email if anything was granted
        if (!empty($granted) && !empty($order->email)) {
            $this->sendDownloadEmail($order, $orderId, $granted);
        }
    }

    /**
     * Send download delivery email to customer
     */
    private function sendDownloadEmail(object $order, int $orderId, array $downloads): void
    {
        $subject  = Config::get('instinctor_dd_email_subject', __('Your download is ready', 'instinctor'));
        $siteName = get_bloginfo('name');
        $name     = $order->first_name ?: 'there';

        $rows = '';
        foreach ($downloads as $dl) {
            $meta = '';
            if ($dl['limit'])  $meta .= ' · ' . $dl['limit'] . ' download' . ($dl['limit'] > 1 ? 's' : '');
            if ($dl['expiry']) $meta .= ' · Expires ' . $dl['expiry'];
            $rows .= '<tr><td style="padding:10px 0;border-bottom:1px solid #f0f0f0">';
            $rows .= '<strong style="color:#1d1d1f">' . esc_html($dl['name']) . '</strong>';
            if ($meta) $rows .= '<br><small style="color:#888">' . esc_html(ltrim($meta, ' · ')) . '</small>';
            $rows .= '</td><td style="padding:10px 0;border-bottom:1px solid #f0f0f0;text-align:right">';
            $rows .= '<a href="' . esc_url($dl['download_url']) . '" style="display:inline-block;padding:6px 16px;background:#1d1d1f;color:#fff;text-decoration:none;border-radius:4px;font-size:13px">Download</a>';
            $rows .= '</td></tr>';
        }

        $html = '<!DOCTYPE html><html><body style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f9f9f9;margin:0;padding:32px 16px">';
        $html .= '<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:8px;padding:32px">';
        $html .= '<h1 style="font-size:22px;font-weight:600;margin:0 0 8px">' . esc_html($subject) . '</h1>';
        $html .= '<p style="color:#555;margin:0 0 24px">Hi ' . esc_html($name) . ', your order #' . esc_html($order->order_number ?? $orderId) . ' is confirmed. Click below to download your files.</p>';
        $html .= '<table width="100%" cellpadding="0" cellspacing="0">' . $rows . '</table>';
        $html .= '<p style="color:#888;font-size:12px;margin:24px 0 0">Links are unique to your order. Do not share them., ' . esc_html($siteName) . '</p>';
        $html .= '</div></body></html>';

        \Instinctor\Services\EmailService::sendDirectEmail($order->email, $subject, $html);
    }

    // =========================================================================
    // CUSTOMER DOWNLOADS
    // =========================================================================

    /**
     * Get all active download permissions for a customer
     */
    public function getCustomerDownloads(int $customerId): array
    {
        $p = DB::prefix();
        return DB::get_results(
            DB::prepare(
                "SELECT
                    dp.id           AS permission_id,
                    dp.download_id,
                    dp.order_id,
                    dp.download_count,
                    dp.access_granted,
                    dp.access_expires,
                    d.name          AS file_name,
                    d.download_limit,
                    p.name          AS product_name,
                    p.id            AS product_id
                FROM {$p}instinctor_download_permissions dp
                JOIN {$p}instinctor_downloads d ON d.id = dp.download_id
                JOIN {$p}instinctor_products p ON p.id = d.product_id
                WHERE dp.customer_id = %d
                  AND (dp.access_expires IS NULL OR dp.access_expires > NOW())
                ORDER BY dp.access_granted DESC",
                $customerId
            ),
            ARRAY_A
        ) ?: [];
    }

    /**
     * Get download permissions for a specific order (for order confirmation page)
     */
    public function getOrderDownloads(int $orderId, int $customerId): array
    {
        $p = DB::prefix();
        return DB::get_results(
            DB::prepare(
                "SELECT
                    dp.id           AS permission_id,
                    dp.download_id,
                    dp.download_count,
                    dp.access_expires,
                    d.name          AS file_name,
                    d.download_limit,
                    p.name          AS product_name
                FROM {$p}instinctor_download_permissions dp
                JOIN {$p}instinctor_downloads d ON d.id = dp.download_id
                JOIN {$p}instinctor_products p ON p.id = d.product_id
                WHERE dp.order_id = %d AND dp.customer_id = %d
                ORDER BY p.name ASC, d.sort_order ASC",
                $orderId,
                $customerId
            ),
            ARRAY_A
        ) ?: [];
    }

    // =========================================================================
    // SECURE FILE SERVING
    // =========================================================================

    /**
     * Generate a signed download URL for a permission
     */
    public function generateDownloadUrl(int $permissionId, int $customerId): string
    {
        $token = Session::token('instinctor_dl_' . $permissionId . '_' . $customerId);
        return add_query_arg([
            'action' => 'instinctor_download_file',
            'pid'    => $permissionId,
            'cid'    => $customerId,
            'token'  => $token,
        ], admin_url('admin-ajax.php'));
    }

    /**
     * Serve a file download (AJAX handler)
     */
    public function serveDownload(): void
    {
        $p = DB::prefix();

        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin Session::check/verify HMAC nonce (WPCS cannot recognise it), inputs sanitize_*'d + wp_unslash'd
        $permissionId = absint(wp_unslash($_GET['pid'] ?? 0));
        // phpcs:enable WordPress.Security.NonceVerification
        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin Session::check/verify HMAC nonce (WPCS cannot recognise it), inputs sanitize_*'d + wp_unslash'd
        $customerId   = absint(wp_unslash($_GET['cid'] ?? 0));
        // phpcs:enable WordPress.Security.NonceVerification
        // phpcs:disable WordPress.Security.NonceVerification -- request authenticated via the plugin Session::check/verify HMAC nonce (WPCS cannot recognise it), inputs sanitize_*'d + wp_unslash'd
        $token        = sanitize_text_field(wp_unslash($_GET['token'] ?? ''));
        // phpcs:enable WordPress.Security.NonceVerification

        // Verify nonce
        if (!wp_verify_nonce($token, 'instinctor_dl_' . $permissionId . '_' . $customerId)) {
            wp_die('Invalid or expired download link.', 'Download Error', ['response' => 403]);
        }

        // Load permission
        $perm = DB::get_row(
            DB::prepare(
                "SELECT dp.*, d.file_url, d.download_limit, d.name AS file_name
                 FROM {$p}instinctor_download_permissions dp
                 JOIN {$p}instinctor_downloads d ON d.id = dp.download_id
                 WHERE dp.id = %d AND dp.customer_id = %d",
                $permissionId,
                $customerId
            )
        );

        if (!$perm) {
            wp_die('Download not found.', 'Download Error', ['response' => 404]);
        }

        // Check expiry
        if ($perm->access_expires && strtotime($perm->access_expires) < time()) {
            wp_die('This download link has expired.', 'Download Expired', ['response' => 410]);
        }

        // Check download limit
        if ($perm->download_limit !== null && $perm->download_count >= (int)$perm->download_limit) {
            wp_die('Download limit reached.', 'Download Limit', ['response' => 403]);
        }

        // Subscription-gated access. If this permission's order is tied to a
        // subscription (either initial order with transaction_id = sub_xxx,
        // or renewal order with meta.subscription_id), the file is only
        // served while the subscription is in good standing. One-time
        // purchases have no matching row and skip this check — they retain
        // permanent access (subject to the expiry / download_limit checks
        // above).
        $subRow = DB::get_row(DB::prepare(
            "SELECT s.status, s.cancel_at_period_end, s.current_period_end
             FROM {$p}instinctor_subscriptions s
             JOIN {$p}instinctor_orders o ON (
                 o.transaction_id = s.external_id
                 OR CAST(JSON_UNQUOTE(JSON_EXTRACT(o.meta, '$.subscription_id')) AS UNSIGNED) = s.id
             )
             WHERE o.id = %d
             LIMIT 1",
            (int) $perm->order_id
        ));
        if ($subRow) {
            // 'active' covers Stripe's trialing+active (mapped in mapStripeStatus()).
            // 'past_due' = Stripe is still retrying — keep access during grace period.
            // Stripe eventually transitions failed past_due to canceled, which fires
            // customer.subscription.deleted and we revoke explicitly there.
            $okStatuses = ['active', 'past_due'];
            if (!in_array((string) $subRow->status, $okStatuses, true)) {
                wp_die(
                    'Your subscription is no longer active. Renew to restore access.',
                    'Subscription Inactive',
                    ['response' => 403]
                );
            }
        }

        // Increment counter
        DB::update(
            DB::prefix() . 'instinctor_download_permissions',
            ['download_count' => $perm->download_count + 1],
            ['id' => $permissionId]
        );

        // Serve the file
        $fileUrl  = $perm->file_url;
        $fileName = sanitize_file_name($perm->file_name);

        // If it's a local upload, use readfile for security
        $baseUrl = Storage::baseurl();
        if (strpos($fileUrl, $baseUrl) === 0) {
            $filePath = Storage::path(ltrim(str_replace($baseUrl, '', $fileUrl), '/'));
            if (file_exists($filePath)) {
                $mimeType = Storage::mime($filePath);
                header('Content-Type: ' . $mimeType);
                header('Content-Disposition: attachment; filename="' . $fileName . '"');
                header('Content-Length: ' . filesize($filePath));
                header('Cache-Control: no-cache, must-revalidate');
                // phpcs:disable WordPress.WP.AlternativeFunctions -- native filesystem/string ops required for standalone, no WP_Filesystem in this path
                readfile($filePath);
                // phpcs:enable WordPress.WP.AlternativeFunctions
                exit;
            }
        }

        // External URL — redirect (less secure but functional for CDN files)
        wp_safe_redirect($fileUrl);
        exit;
    }

    // =========================================================================
    // SUBSCRIPTION REVOCATION
    // =========================================================================

    /**
     * Revoke all download permissions tied to a cancelled subscription.
     *
     * Sets `access_expires = NOW()` on every permission row whose order's
     * transaction_id matches the subscription's external_id. Existing
     * permission rows are kept (not deleted) so the audit trail survives —
     * the serveDownload() expiry check enforces the cutoff. Idempotent:
     * already-expired rows are not re-touched.
     *
     * Called from:
     *   - SubscriptionService::cancelSubscription($id, immediately=true)
     *   - SubscriptionService Stripe webhook 'customer.subscription.deleted'
     *
     * Returns the number of permission rows revoked.
     */
    public function revokeSubscriptionAccess(int $subscriptionId): int
    {
        $p = DB::prefix();

        $sub = DB::get_row(DB::prepare(
            "SELECT id, external_id FROM {$p}instinctor_subscriptions WHERE id = %d",
            $subscriptionId
        ));
        if (!$sub || empty($sub->external_id)) {
            return 0;
        }

        // Find every order whose transaction_id is this subscription's
        // external_id — that's the initial order PLUS every renewal order
        // createRenewalOrder() inserted (renewal transaction_id = invoice
        // id, not subscription id, so renewals don't match here; only the
        // initial order does, which is the order that granted the original
        // permission rows; renewal orders re-grant additional permission
        // rows under their own order_id which we also catch below by
        // joining downloads → products → subscription.product_id).
        //
        // Use the broader join: any permission for (customer_id, product_id)
        // matching the subscription. This covers both the initial order
        // and all renewals uniformly.
        return (int) DB::query(DB::prepare(
            "UPDATE {$p}instinctor_download_permissions dp
             JOIN {$p}instinctor_downloads d ON d.id = dp.download_id
             SET dp.access_expires = NOW()
             WHERE dp.customer_id = (SELECT customer_id FROM {$p}instinctor_subscriptions WHERE id = %d)
               AND d.product_id   = (SELECT product_id  FROM {$p}instinctor_subscriptions WHERE id = %d)
               AND (dp.access_expires IS NULL OR dp.access_expires > NOW())",
            $subscriptionId,
            $subscriptionId
        ));
    }

    // =========================================================================
    // ADMIN HELPERS
    // =========================================================================

    /**
     * Get download stats for admin display
     */
    public function getDownloadStats(): array
    {
        $p = DB::prefix();
        return [
            'total_downloads'  => (int) DB::get_var("SELECT SUM(download_count) FROM {$p}instinctor_download_permissions") ?: 0,
            'active_files'     => (int) DB::get_var("SELECT COUNT(*) FROM {$p}instinctor_downloads") ?: 0,
            'customers_with_access' => (int) DB::get_var("SELECT COUNT(DISTINCT customer_id) FROM {$p}instinctor_download_permissions WHERE (access_expires IS NULL OR access_expires > NOW())") ?: 0,
        ];
    }

    /**
     * Register an AJAX handler for both WordPress and standalone modes.
     * On WordPress: add_action('wp_ajax_' . $action)
     * On standalone: AjaxDispatcher::register($action)
     */
    private function registerAjax(string $action, callable $handler): void
    {
        if (!defined('INSTINCTOR_ENV') || INSTINCTOR_ENV === 'wordpress') {
            add_action('wp_ajax_' . $action, $handler);
        } else {
            \Instinctor\Core\AjaxDispatcher::register($action, $handler);
        }
    }

}
