<?php

namespace Licenzo\Services\Updates;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

use Licenzo\Model\License;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Product;
use Licenzo\Model\ProductVariation;
use Licenzo\Helper\Action;
use Licenzo\Model\LicenseEvent;
use Licenzo\Services\Media\FileStreamer;
use WP_REST_Response;

class ProductUpdateService
{

    /**
     * @var ProductReleaseService
     */
    private $releaseService;

    public function __construct(?ProductReleaseService $releaseService = null)
    {
        $this->releaseService = $releaseService ?? new ProductReleaseService();
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public function checkForUpdate(string $licenseKey, string $productUuid, string $variationUuid, array $context = []): array
    {
        [$license, $product, $variation] = $this->resolveEntitledContext($licenseKey, $productUuid, $variationUuid);

        $channel = $this->sanitizeChannel((string) ($context['channel'] ?? ProductReleaseService::CHANNEL_STABLE));
        $currentVersion = $this->extractCurrentVersion($context);

        $release = $this->releaseService->findLatestAvailableRelease($product, $variation, $channel);
        if (!$release) {
            return [
                'license' => $this->licenseSummary($license),
                'product' => $this->productSummary($product, $variation),
                'channel' => $channel,
                'current_version' => $currentVersion,
                'update_available' => false,
                'message' => 'No published release available for this variation.',
            ];
        }

        $latestVersion = (string) ($release['version'] ?? '');
        $updateAvailable = $currentVersion === '' ? true : version_compare($latestVersion, $currentVersion, '>');

        $package = is_array($release['selected_package'] ?? null) ? $release['selected_package'] : $this->releaseService->getVariationPackage($release, $variation);
        if (!$package) {
            throw new \RuntimeException('Release package is missing for the requested variation.');
        }

        $downloadUrl = $this->buildDownloadUrl($license, $product, $variation, (string) $release['uuid']);
        $sdkPayload = $this->buildSdkPayload($product, $variation, $release, $package, $downloadUrl);

        return [
            'license' => $this->licenseSummary($license),
            'product' => $this->productSummary($product, $variation),
            'channel' => $channel,
            'current_version' => $currentVersion,
            'latest_version' => $latestVersion,
            'update_available' => $updateAvailable,
            'release' => $this->publicReleasePayload($release, $package),
            'sdk' => $sdkPayload,
        ];
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public function getUpdateInfo(string $licenseKey, string $productUuid, string $variationUuid, array $context = []): array
    {
        $result = $this->checkForUpdate($licenseKey, $productUuid, $variationUuid, $context);
        if (!empty($result['update_available'])) {
            return $result;
        }

        // If current version is already latest or absent, still expose latest release metadata for SDK modals.
        [$license, $product, $variation] = $this->resolveEntitledContext($licenseKey, $productUuid, $variationUuid);
        $channel = $this->sanitizeChannel((string) ($context['channel'] ?? ProductReleaseService::CHANNEL_STABLE));
        $release = $this->releaseService->findLatestAvailableRelease($product, $variation, $channel);

        if (!$release) {
            return $result;
        }

        $package = is_array($release['selected_package'] ?? null) ? $release['selected_package'] : $this->releaseService->getVariationPackage($release, $variation);
        if (!$package) {
            return $result;
        }

        $downloadUrl = $this->buildDownloadUrl($license, $product, $variation, (string) $release['uuid']);
        $result['release'] = $this->publicReleasePayload($release, $package);
        $result['sdk'] = $this->buildSdkPayload($product, $variation, $release, $package, $downloadUrl);
        $result['latest_version'] = (string) ($release['version'] ?? '');

        return $result;
    }

    /**
     * Handle a download request verified by a one-time token.
     *
     * URL: GET /updates/download/{token}?release={uuid}&lid={id}&vid={id}
     * Token is a cryptographically random, single-use string generated in buildDownloadUrl().
     *
     * @return array<string,mixed>|void
     */
    public function handleDownload(string $token, string $releaseUuid, int $licenseId, int $variationId)
    {
        $token = sanitize_text_field($token);
        $releaseUuid = sanitize_text_field($releaseUuid);

        if ($token === '' || $licenseId <= 0 || $variationId <= 0 || $releaseUuid === '') {
            return [
                'success' => false,
                'message' => 'Missing required download parameters.',
                'status' => 400,
            ];
        }

        $tokenService = new DownloadTokenService();
        $tokenRecord = $tokenService->validate($token, $licenseId, $variationId, $releaseUuid);

        if (!$tokenRecord) {
            return [
                'success' => false,
                'message' => 'Invalid or expired download token.',
                'status' => 401,
            ];
        }

        $license = License::find($licenseId);
        if (!$license) {
            return [
                'success' => false,
                'message' => 'License not found.',
                'status' => 404,
            ];
        }

        $variation = ProductVariation::find($variationId);
        if (!$variation) {
            return [
                'success' => false,
                'message' => 'Variation not found.',
                'status' => 404,
            ];
        }

        $product = Product::find((int) $variation->product_id);
        if (!$product) {
            return [
                'success' => false,
                'message' => 'Product not found.',
                'status' => 404,
            ];
        }

        $this->assertLicenseEntitled($license, $product, $variation);

        $release = $this->releaseService->getRelease((int) $product->id, $releaseUuid);
        if (!$release) {
            return [
                'success' => false,
                'message' => 'Release not found.',
                'status' => 404,
            ];
        }

        if ((string) ($release['status'] ?? '') !== ProductReleaseService::STATUS_PUBLISHED || empty($release['is_available'])) {
            return [
                'success' => false,
                'message' => 'Release is not available for download.',
                'status' => 403,
            ];
        }

        $package = $this->releaseService->getVariationPackage($release, $variation);
        if (!$package) {
            return [
                'success' => false,
                'message' => 'Release package is unavailable for this variation.',
                'status' => 404,
            ];
        }

        $media = is_array($package['media'] ?? null) ? $package['media'] : [];
        $filePath = $this->resolveLocalFilePath($media);

        if ($filePath === '' || !file_exists($filePath) || !is_readable($filePath)) {
            return [
                'success' => false,
                'message' => 'Release package file could not be found on server.',
                'status' => 500,
            ];
        }

        $fileName = sanitize_file_name((string) ($media['download_name'] ?? basename($filePath)));
        $mimeType = (string) ($media['mime_type'] ?? 'application/octet-stream');
        if ($mimeType === '') {
            $mimeType = 'application/octet-stream';
        }
        $fileSize = filesize($filePath);

        $tokenService->consume($tokenRecord['meta']);

        $this->logDownloadEvent($license, $product, $variation, $release);

        $this->streamFile($filePath, $fileName, $mimeType, $fileSize);
    }

    /**
     * Resolve the local server file path from a media payload.
     */
    private function resolveLocalFilePath(array $media): string
    {
        $attachmentId = absint($media['attachment_id'] ?? 0);
        if ($attachmentId > 0) {
            $path = get_attached_file($attachmentId);
            if (is_string($path) && $path !== '') {
                return $path;
            }
        }

        return '';
    }

    /**
     * Stream a file to the client and terminate.
     *
     * @param string $filePath
     * @param string $fileName
     * @param string $mimeType
     * @param int|false $fileSize
     * @return never
     */
    private function streamFile(string $filePath, string $fileName, string $mimeType, $fileSize): void
    {
        FileStreamer::stream($filePath, $fileName, $mimeType, $fileSize ? (int) $fileSize : null);
    }

    private function logDownloadEvent(License $license, Product $product, ProductVariation $variation, array $release): void
    {
        try {
            LicenseEvent::logEvent([
                'license_id' => (int) $license->id,
                'product_id' => (int) $product->id,
                'variation_id' => (int) $variation->id,
                'product_name' => (string) $product->name,
                'variation_name' => (string) $variation->name,
                'event_type' => LicenseEvent::EVENT_DOWNLOADED,
                'event_source' => LicenseEvent::SOURCE_API,
                'event_data' => [
                    'download_source' => 'sdk',
                    'release_uuid' => (string) ($release['uuid'] ?? ''),
                    'version' => (string) ($release['version'] ?? ''),
                    'channel' => (string) ($release['channel'] ?? 'stable'),
                ],
                'ip_address' => sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')),
                'response_status' => LicenseEvent::STATUS_SUCCESS,
                'response_message' => 'File downloaded via SDK update',
            ]);
        } catch (\Throwable $e) {
            // Logging failure must not block the download
        }
    }

    private function extractCurrentVersion(array $context): string
    {
        $candidates = [
            'current_version',
            'installed_version',
            'version',
        ];

        foreach ($candidates as $candidate) {
            if (!isset($context[$candidate])) {
                continue;
            }

            $value = sanitize_text_field((string) $context[$candidate]);
            if ($value !== '') {
                return $value;
            }
        }

        return '';
    }

    private function sanitizeChannel(string $channel): string
    {
        $channel = strtolower(trim($channel));
        $allowed = [
            ProductReleaseService::CHANNEL_STABLE,
            ProductReleaseService::CHANNEL_BETA,
            ProductReleaseService::CHANNEL_ALPHA,
            ProductReleaseService::CHANNEL_RC,
        ];

        return in_array($channel, $allowed, true) ? $channel : ProductReleaseService::CHANNEL_STABLE;
    }

    /**
     * @return array{0:License,1:Product,2:ProductVariation}
     */
    private function resolveEntitledContext(string $licenseKey, string $productUuid, string $variationUuid): array
    {
        $licenseKey = sanitize_text_field($licenseKey);
        $productUuid = sanitize_text_field($productUuid);
        $variationUuid = sanitize_text_field($variationUuid);

        $license = License::where('license_key', $licenseKey)->first();
        if (!$license) {
            throw new \RuntimeException('License not found.');
        }

        $product = Product::where('uuid', $productUuid)->first();
        if (!$product) {
            throw new \RuntimeException('Product not found.');
        }

        $variation = ProductVariation::where('uuid', $variationUuid)
            ->where('product_id', (int) $product->id)
            ->first();
        if (!$variation) {
            throw new \RuntimeException('Variation not found.');
        }

        $this->assertLicenseEntitled($license, $product, $variation);

        return [$license, $product, $variation];
    }

    private function assertLicenseEntitled(License $license, Product $product, ProductVariation $variation): void
    {
        if ((string) $license->status !== License::STATUS_ACTIVE) {
            throw new \RuntimeException('License is not active.');
        }

        if (!empty($license->expire_date) && strtotime((string) $license->expire_date) < strtotime(gmdate('Y-m-d'))) {
            $license->load('customer');

            Action::do('notification_license_expiration', [
                'source' => 'product_update_service',
                'reason' => 'license_expired_update_check',
                'customer' => $license->customer ? (array) $license->customer->toArray() : [],
                'license' => (array) $license->toArray(),
                'product' => (array) $product->toArray(),
                'variation' => (array) $variation->toArray(),
                'meta' => [
                    'expire_date' => gmdate('Y-m-d', strtotime((string) $license->expire_date)),
                ],
            ]);

            throw new \RuntimeException('License has expired.');
        }

        $licenseProduct = LicenseProduct::where('license_id', (int) $license->id)
            ->where('product_id', (int) $product->id)
            ->where('variation_id', (int) $variation->id)
            ->first();

        if (!$licenseProduct) {
            throw new \RuntimeException('License is not entitled to this product variation.');
        }
    }

    private function buildDownloadUrl(License $license, Product $product, ProductVariation $variation, string $releaseUuid): string
    {
        $tokenService = new DownloadTokenService();
        $token = $tokenService->generate((int) $license->id, (int) $variation->id, sanitize_text_field($releaseUuid));

        return rest_url('licenzo/v1/updates/download/' . rawurlencode($token)) . '?' . http_build_query([
            'release' => sanitize_text_field($releaseUuid),
            'lid' => (int) $license->id,
            'vid' => (int) $variation->id,
        ]);
    }

    /**
     * @param array<string,mixed> $release
     * @param array<string,mixed> $package
     * @return array<string,mixed>
     */
    private function buildSdkPayload(Product $product, ProductVariation $variation, array $release, array $package, string $downloadUrl): array
    {
        $releaseSdkMeta = is_array($release['sdk_meta'] ?? null) ? $release['sdk_meta'] : [];
        $packageSdkMeta = is_array($package['sdk_meta'] ?? null) ? $package['sdk_meta'] : [];
        $sdkMeta = array_replace_recursive($releaseSdkMeta, $packageSdkMeta);

        $sections = is_array($sdkMeta['sections'] ?? null) ? $sdkMeta['sections'] : [];
        if (!isset($sections['changelog']) || trim((string) $sections['changelog']) === '') {
            $sections['changelog'] = (string) ($release['changelog'] ?? '');
        }

        $slug = sanitize_title((string) ($sdkMeta['slug'] ?? ''));
        if ($slug === '') {
            $slug = sanitize_title((string) $product->slug ?: (string) $product->name);
        }

        $pluginBasename = sanitize_text_field((string) ($sdkMeta['plugin'] ?? ''));
        $homepage = esc_url_raw((string) ($sdkMeta['homepage'] ?? $sdkMeta['url'] ?? ''));
        $url = esc_url_raw((string) ($sdkMeta['url'] ?? $homepage));

        return [
            'slug' => $slug,
            'plugin' => $pluginBasename,
            'version' => (string) ($release['version'] ?? ''),
            'package' => $downloadUrl,
            'tested' => sanitize_text_field((string) ($sdkMeta['tested'] ?? '')),
            'requires' => sanitize_text_field((string) ($sdkMeta['requires'] ?? '')),
            'requires_php' => sanitize_text_field((string) ($sdkMeta['requires_php'] ?? '')),
            'last_updated' => sanitize_text_field((string) ($sdkMeta['last_updated'] ?? (($release['release_date'] ?? '') . ' 00:00:00'))),
            'homepage' => $homepage,
            'url' => $url,
            'upgrade_notice' => sanitize_textarea_field((string) ($sdkMeta['upgrade_notice'] ?? '')),
            'sections' => $sections,
            'banners' => is_array($sdkMeta['banners'] ?? null) ? $sdkMeta['banners'] : [],
            'icons' => is_array($sdkMeta['icons'] ?? null) ? $sdkMeta['icons'] : [],
            'compatibility' => is_array($sdkMeta['compatibility'] ?? null) ? $sdkMeta['compatibility'] : [],
            'extra' => is_array($sdkMeta['extra'] ?? null) ? $sdkMeta['extra'] : [],
            'variation' => [
                'id' => (int) $variation->id,
                'uuid' => (string) $variation->uuid,
                'name' => (string) $variation->name,
            ],
        ];
    }

    /**
     * @param array<string,mixed> $release
     * @param array<string,mixed> $package
     * @return array<string,mixed>
     */
    private function publicReleasePayload(array $release, array $package): array
    {
        return [
            'uuid' => (string) ($release['uuid'] ?? ''),
            'title' => (string) ($release['title'] ?? ''),
            'version' => (string) ($release['version'] ?? ''),
            'status' => (string) ($release['status'] ?? ''),
            'channel' => (string) ($release['channel'] ?? ProductReleaseService::CHANNEL_STABLE),
            'is_available' => !empty($release['is_available']),
            'release_date' => (string) ($release['release_date'] ?? ''),
            'changelog' => (string) ($release['changelog'] ?? ''),
            'sdk_meta' => is_array($release['sdk_meta'] ?? null) ? $release['sdk_meta'] : [],
            'variation_package' => [
                'variation_id' => (int) ($package['variation_id'] ?? 0),
                'variation_uuid' => (string) ($package['variation_uuid'] ?? ''),
                'variation_name' => (string) ($package['variation_name'] ?? ''),
                'media' => is_array($package['media'] ?? null) ? $package['media'] : [],
                'woo_asset' => is_array($package['woo_asset'] ?? null) ? $package['woo_asset'] : [],
            ],
            'updated_at' => (string) ($release['updated_at'] ?? ''),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function licenseSummary(License $license): array
    {
        return [
            'id' => (int) $license->id,
            'status' => (string) $license->status,
            'expire_date' => $license->expire_date ? (string) $license->expire_date : null,
            'support_till' => $license->support_till ? (string) $license->support_till : null,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function productSummary(Product $product, ProductVariation $variation): array
    {
        return [
            'id' => (int) $product->id,
            'uuid' => (string) $product->uuid,
            'name' => (string) $product->name,
            'variation' => [
                'id' => (int) $variation->id,
                'uuid' => (string) $variation->uuid,
                'name' => (string) $variation->name,
            ],
        ];
    }

}
