<?php

namespace Licenzo\Services\Updates;

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

use Licenzo\Model\Meta;
use Licenzo\Model\Product;
use Licenzo\Model\ProductVariation;
use Licenzo\Services\Integrations\ProviderAliasService;
use Licenzo\Services\Media\ReleaseMediaAdapterInterface;
use Licenzo\Services\Media\WordPressMediaLibraryReleaseMediaAdapter;

class ProductReleaseService
{
    public const META_ENTITY_TYPE = 'product_release';
    public const META_KEY_PREFIX = 'release:';

    public const STATUS_DRAFT = 'draft';
    public const STATUS_PUBLISHED = 'published';
    public const STATUS_ARCHIVED = 'archived';

    public const CHANNEL_STABLE = 'stable';
    public const CHANNEL_BETA = 'beta';
    public const CHANNEL_ALPHA = 'alpha';
    public const CHANNEL_RC = 'rc';

    /**
     * @var ReleaseMediaAdapterInterface
     */
    private $mediaAdapter;

    public function __construct(?ReleaseMediaAdapterInterface $mediaAdapter = null)
    {
        $this->mediaAdapter = $mediaAdapter ?? new WordPressMediaLibraryReleaseMediaAdapter();
    }

    /**
     * @param array<string,mixed> $filters
     * @return array<string,mixed>
     */
    public function listForProduct(int $productId, array $filters = []): array
    {
        $product = $this->requireProduct($productId);
        $records = $this->loadAllReleaseRecords($product->id);

        $status = sanitize_text_field((string) ($filters['status'] ?? ''));
        $channel = sanitize_text_field((string) ($filters['channel'] ?? ''));
        $search = sanitize_text_field((string) ($filters['search'] ?? ''));
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = min(100, max(1, (int) ($filters['per_page'] ?? 15)));

        $filtered = array_values(array_filter($records, function (array $record) use ($status, $channel, $search): bool {
            if ($status !== '' && $status !== 'all' && (string) ($record['status'] ?? '') !== $status) {
                return false;
            }

            if ($channel !== '' && $channel !== 'all' && (string) ($record['channel'] ?? '') !== $channel) {
                return false;
            }

            if ($search !== '') {
                $haystack = strtolower(
                    (string) ($record['version'] ?? '') . ' ' .
                    (string) ($record['changelog'] ?? '') . ' ' .
                    (string) ($record['title'] ?? '')
                );
                if (strpos($haystack, strtolower($search)) === false) {
                    return false;
                }
            }

            return true;
        }));

        usort($filtered, function (array $a, array $b): int {
            $versionCompare = version_compare((string) ($b['version'] ?? '0'), (string) ($a['version'] ?? '0'));
            if ($versionCompare !== 0) {
                return $versionCompare;
            }

            $aDate = strtotime((string) ($a['release_date'] ?? '')) ?: 0;
            $bDate = strtotime((string) ($b['release_date'] ?? '')) ?: 0;
            if ($aDate !== $bDate) {
                return $bDate <=> $aDate;
            }

            return strcmp((string) ($b['updated_at'] ?? ''), (string) ($a['updated_at'] ?? ''));
        });

        $total = count($filtered);
        $lastPage = max(1, (int) ceil($total / $perPage));
        $page = min($page, $lastPage);
        $offset = ($page - 1) * $perPage;
        $items = array_slice($filtered, $offset, $perPage);

        return [
            'product' => [
                'id' => (int) $product->id,
                'uuid' => (string) $product->uuid,
                'name' => (string) $product->name,
            ],
            'items' => array_values($items),
            'pagination' => [
                'total' => $total,
                'per_page' => $perPage,
                'current_page' => $page,
                'last_page' => $lastPage,
            ],
            'filters' => [
                'status' => $status,
                'channel' => $channel,
                'search' => $search,
            ],
        ];
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function createRelease(int $productId, array $payload): array
    {
        $product = $this->requireProduct($productId);
        $release = $this->normalizeReleasePayload($product, $payload, null);
        $release['uuid'] = wp_generate_uuid4();
        $release['created_at'] = gmdate('Y-m-d H:i:s');
        $release['updated_at'] = $release['created_at'];

        $this->assertUniqueVersionForChannel($product->id, $release['version'], $release['channel'], null);
        $this->persistReleaseRecord($product->id, $release);

        $warnings = $this->syncReleaseToWooAssetsIfNeeded($product, $release);
        $release['sync_warnings'] = array_values($warnings);
        $release['updated_at'] = gmdate('Y-m-d H:i:s');
        $this->persistReleaseRecord($product->id, $release);

        if ($release['status'] === static::STATUS_PUBLISHED) {
            $this->archivePreviousReleasesOnChannel($product->id, $release['channel'], $release['uuid']);
        }

        return $release;
    }

    /**
     * @param array<string,mixed> $payload
     * @return array<string,mixed>
     */
    public function updateRelease(int $productId, string $releaseUuid, array $payload): array
    {
        $product = $this->requireProduct($productId);
        $existing = $this->getRelease($product->id, $releaseUuid);
        if (!$existing) {
            throw new \RuntimeException('Release not found.');
        }

        $wasPublished = (string) ($existing['status'] ?? '') === static::STATUS_PUBLISHED;

        $release = $this->normalizeReleasePayload($product, $payload, $existing);
        $release['uuid'] = (string) $existing['uuid'];
        $release['created_at'] = (string) ($existing['created_at'] ?? gmdate('Y-m-d H:i:s'));
        $release['updated_at'] = gmdate('Y-m-d H:i:s');

        $isPublished = $release['status'] === static::STATUS_PUBLISHED;

        $this->assertUniqueVersionForChannel($product->id, $release['version'], $release['channel'], $release['uuid']);

        if ($wasPublished && !$isPublished) {
            $this->removeWooDownloadsForRelease($product, $existing);
        }

        $this->persistReleaseRecord($product->id, $release);

        $warnings = $this->syncReleaseToWooAssetsIfNeeded($product, $release);
        $release['sync_warnings'] = array_values($warnings);
        $release['updated_at'] = gmdate('Y-m-d H:i:s');
        $this->persistReleaseRecord($product->id, $release);

        if ($isPublished) {
            $this->archivePreviousReleasesOnChannel($product->id, $release['channel'], $release['uuid']);
        }

        return $release;
    }

    public function deleteRelease(int $productId, string $releaseUuid): bool
    {
        $product = $this->requireProduct($productId);
        $release = $this->getRelease($product->id, $releaseUuid);

        if (!$release) {
            return false;
        }

        $this->removeWooDownloadsForRelease($product, $release);

        $deleted = (bool) Meta::where('entity_type', static::META_ENTITY_TYPE)
            ->where('entity_id', $product->id)
            ->where('meta_key', $this->releaseMetaKey($releaseUuid))
            ->delete();

        if ($deleted) {
            $this->cleanupReleaseMedia($product->id, $release);
        }

        return $deleted;
    }

    /**
     * @return array<string,mixed>|null
     */
    public function getRelease(int $productId, string $releaseUuid): ?array
    {
        $releaseUuid = sanitize_text_field($releaseUuid);
        if ($releaseUuid === '') {
            return null;
        }

        $row = Meta::where('entity_type', static::META_ENTITY_TYPE)
            ->where('entity_id', $productId)
            ->where('meta_key', $this->releaseMetaKey($releaseUuid))
            ->first();

        if (!$row) {
            return null;
        }

        return $this->decodeReleaseMetaValue((string) $row->meta_value);
    }

    public function findLatestAvailableRelease(Product $product, ProductVariation $variation, string $channel = self::CHANNEL_STABLE): ?array
    {
        $records = $this->loadAllReleaseRecords((int) $product->id);
        $channel = $this->sanitizeChannel($channel);

        $candidates = [];
        foreach ($records as $record) {
            if ((string) ($record['status'] ?? '') !== static::STATUS_PUBLISHED) {
                continue;
            }

            if (!$this->toBool($record['is_available'] ?? true)) {
                continue;
            }

            if ((string) ($record['channel'] ?? static::CHANNEL_STABLE) !== $channel) {
                continue;
            }

            $package = $this->getVariationPackage($record, $variation);
            if (!$package) {
                continue;
            }

            $record['selected_package'] = $package;
            $candidates[] = $record;
        }

        if (empty($candidates)) {
            return null;
        }

        usort($candidates, function (array $a, array $b): int {
            $v = version_compare((string) ($b['version'] ?? '0'), (string) ($a['version'] ?? '0'));
            if ($v !== 0) {
                return $v;
            }

            $aDate = strtotime((string) ($a['release_date'] ?? '')) ?: 0;
            $bDate = strtotime((string) ($b['release_date'] ?? '')) ?: 0;
            return $bDate <=> $aDate;
        });

        return $candidates[0];
    }

    /**
     * @param array<string,mixed> $package
     */
    public function resolvePackagePublicUrl(array $package): string
    {
        $media = is_array($package['media'] ?? null) ? $package['media'] : [];
        return $this->mediaAdapter->resolvePublicUrl($media);
    }

    /**
     * @param array<string,mixed> $release
     * @return array<string,mixed>|null
     */
    public function getVariationPackage(array $release, ProductVariation $variation): ?array
    {
        $packages = is_array($release['variation_packages'] ?? null) ? $release['variation_packages'] : [];
        foreach ($packages as $package) {
            if (!is_array($package)) {
                continue;
            }

            $packageVariationId = (int) ($package['variation_id'] ?? 0);
            $packageVariationUuid = (string) ($package['variation_uuid'] ?? '');

            if ($packageVariationId === (int) $variation->id || ($packageVariationUuid !== '' && $packageVariationUuid === (string) $variation->uuid)) {
                return $package;
            }
        }

        return null;
    }

    /**
     * @param array<string,mixed> $filters
     * @return array<int,array<string,mixed>>
     */
    public function getAllReleasesForProduct(int $productId, array $filters = []): array
    {
        $records = $this->loadAllReleaseRecords($productId);
        $status = isset($filters['status']) ? $this->sanitizeStatus((string) $filters['status']) : null;
        $channel = isset($filters['channel']) ? $this->sanitizeChannel((string) $filters['channel']) : null;

        $records = array_values(array_filter($records, function (array $record) use ($status, $channel): bool {
            if ($status !== null && (string) ($record['status'] ?? '') !== $status) {
                return false;
            }
            if ($channel !== null && (string) ($record['channel'] ?? '') !== $channel) {
                return false;
            }
            return true;
        }));

        usort($records, function (array $a, array $b): int {
            return version_compare((string) ($b['version'] ?? '0'), (string) ($a['version'] ?? '0'));
        });

        return $records;
    }

    /**
     * Archive all published releases on a channel except the specified one.
     */
    public function archivePreviousReleasesOnChannel(int $productId, string $channel, string $excludeReleaseUuid): int
    {
        $records = $this->loadAllReleaseRecords($productId);
        $archived = 0;

        foreach ($records as $record) {
            if ((string) ($record['uuid'] ?? '') === $excludeReleaseUuid) {
                continue;
            }

            if ((string) ($record['status'] ?? '') !== static::STATUS_PUBLISHED) {
                continue;
            }

            if ((string) ($record['channel'] ?? static::CHANNEL_STABLE) !== $channel) {
                continue;
            }

            $record['status'] = static::STATUS_ARCHIVED;
            $record['updated_at'] = gmdate('Y-m-d H:i:s');
            $this->persistReleaseRecord($productId, $record);
            $archived++;
        }

        return $archived;
    }

    /**
     * Delete orphaned media attachments from a release.
     * Only deletes attachments not referenced by another release of the same product.
     */
    public function cleanupReleaseMedia(int $productId, array $release): int
    {
        $packages = is_array($release['variation_packages'] ?? null) ? $release['variation_packages'] : [];
        $attachmentIds = [];

        foreach ($packages as $package) {
            if (!is_array($package)) {
                continue;
            }
            $aid = absint($package['media']['attachment_id'] ?? 0);
            if ($aid > 0) {
                $attachmentIds[] = $aid;
            }
        }

        if (empty($attachmentIds)) {
            return 0;
        }

        $otherRecords = $this->loadAllReleaseRecords($productId);
        $releaseUuid = (string) ($release['uuid'] ?? '');
        $referencedIds = [];

        foreach ($otherRecords as $other) {
            if ((string) ($other['uuid'] ?? '') === $releaseUuid) {
                continue;
            }
            $otherPackages = is_array($other['variation_packages'] ?? null) ? $other['variation_packages'] : [];
            foreach ($otherPackages as $pkg) {
                if (!is_array($pkg)) {
                    continue;
                }
                $aid = absint($pkg['media']['attachment_id'] ?? 0);
                if ($aid > 0) {
                    $referencedIds[$aid] = true;
                }
            }
        }

        $deleted = 0;
        foreach (array_unique($attachmentIds) as $aid) {
            if (isset($referencedIds[$aid])) {
                continue;
            }
            if (wp_delete_attachment($aid, true)) {
                $deleted++;
            }
        }

        return $deleted;
    }

    /**
     * Remove WooCommerce downloadable files associated with a release.
     *
     * @return array<int,string> Warnings
     */
    public function removeWooDownloadsForRelease(Product $product, array $release): array
    {
        $warnings = [];

        if (!ProviderAliasService::isWoo((string) ($product->provider ?? ''))) {
            return $warnings;
        }

        if (!function_exists('wc_get_product')) {
            return $warnings;
        }

        foreach ($product->variations as $variation) {
            $package = $this->getVariationPackage($release, $variation);
            if (!$package) {
                continue;
            }

            try {
                $this->removeWooDownloadForVariation($product, $variation, $release);
            } catch (\Throwable $e) {
                $warnings[] = sprintf(
                    'Failed to remove WC download for variation %s: %s',
                    (string) $variation->name,
                    $e->getMessage()
                );
            }
        }

        return $warnings;
    }

    private function removeWooDownloadForVariation(Product $product, ProductVariation $variation, array $release): void
    {
        $wooParentProductId = absint($product->provider_id ?? 0);
        if ($wooParentProductId <= 0) {
            return;
        }

        $wooVariationId = absint($variation->provider_id ?? 0);
        $targetProductId = $wooVariationId > 0 ? $wooVariationId : $wooParentProductId;

        $wcProduct = wc_get_product($targetProductId);
        if (!$wcProduct) {
            return;
        }

        $downloadId = $this->buildWooDownloadId(
            (string) ($release['channel'] ?? static::CHANNEL_STABLE),
            (string) ($variation->uuid ?? '')
        );

        $downloads = $wcProduct->get_downloads();
        if (!is_array($downloads) || !isset($downloads[$downloadId])) {
            return;
        }

        unset($downloads[$downloadId]);
        $wcProduct->set_downloads($downloads);

        if (empty($downloads)) {
            $wcProduct->set_downloadable(false);
        }

        $wcProduct->save();
    }

    private function requireProduct(int $productId): Product
    {
        $product = Product::with('variations')->find($productId);
        if (!$product) {
            throw new \RuntimeException('Product not found.');
        }

        return $product;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function loadAllReleaseRecords(int $productId): array
    {
        $rows = Meta::where('entity_type', static::META_ENTITY_TYPE)
            ->where('entity_id', $productId)
            ->where('meta_key', 'LIKE', static::META_KEY_PREFIX . '%')
            ->get();

        $records = [];
        foreach ($rows as $row) {
            $record = $this->decodeReleaseMetaValue((string) $row->meta_value);
            if (!is_array($record) || empty($record['uuid'])) {
                continue;
            }
            $records[] = $record;
        }

        return $records;
    }

    /**
     * @param array<string,mixed>|null $existing
     * @return array<string,mixed>
     */
    private function normalizeReleasePayload(Product $product, array $payload, ?array $existing): array
    {
        $release = is_array($existing) ? $existing : [];

        if (array_key_exists('title', $payload)) {
            $release['title'] = sanitize_text_field((string) $payload['title']);
        } elseif (!isset($release['title'])) {
            $release['title'] = '';
        }

        if (array_key_exists('version', $payload)) {
            $version = sanitize_text_field((string) $payload['version']);
            if ($version === '') {
                throw new \InvalidArgumentException('Release version is required.');
            }
            $release['version'] = $version;
        }

        if (empty($release['version'])) {
            throw new \InvalidArgumentException('Release version is required.');
        }

        if (array_key_exists('status', $payload)) {
            $release['status'] = $this->sanitizeStatus((string) $payload['status']);
        } elseif (!isset($release['status'])) {
            $release['status'] = static::STATUS_DRAFT;
        }

        if (array_key_exists('channel', $payload)) {
            $release['channel'] = $this->sanitizeChannel((string) $payload['channel']);
        } elseif (!isset($release['channel'])) {
            $release['channel'] = static::CHANNEL_STABLE;
        }

        if (array_key_exists('is_available', $payload)) {
            $release['is_available'] = $this->toBool($payload['is_available']);
        } elseif (!isset($release['is_available'])) {
            $release['is_available'] = true;
        }

        if (array_key_exists('release_date', $payload)) {
            $release['release_date'] = $this->normalizeDate((string) $payload['release_date']);
        } elseif (empty($release['release_date'])) {
            $release['release_date'] = gmdate('Y-m-d');
        }

        if (array_key_exists('changelog', $payload)) {
            $release['changelog'] = sanitize_textarea_field((string) $payload['changelog']);
        } elseif (!isset($release['changelog'])) {
            $release['changelog'] = '';
        }

        if (array_key_exists('sdk_meta', $payload)) {
            $release['sdk_meta'] = $this->sanitizeSdkMeta(is_array($payload['sdk_meta']) ? $payload['sdk_meta'] : []);
        } elseif (!isset($release['sdk_meta'])) {
            $release['sdk_meta'] = $this->sanitizeSdkMeta([]);
        }

        if (array_key_exists('metadata', $payload)) {
            $release['metadata'] = $this->sanitizeScalarMap(is_array($payload['metadata']) ? $payload['metadata'] : []);
        } elseif (!isset($release['metadata'])) {
            $release['metadata'] = [];
        }

        if (array_key_exists('variation_packages', $payload)) {
            $packages = is_array($payload['variation_packages']) ? $payload['variation_packages'] : [];
            $release['variation_packages'] = $this->normalizeVariationPackages($product, $packages);
        } elseif (!isset($release['variation_packages'])) {
            $release['variation_packages'] = [];
        }

        if ($release['status'] === static::STATUS_PUBLISHED && $this->toBool($release['is_available'])) {
            $this->assertAllProductVariationsHavePackages($product, $release['variation_packages']);
        }

        $release['product_id'] = (int) $product->id;
        $release['product_uuid'] = (string) $product->uuid;
        $release['product_name'] = (string) $product->name;
        $release['media_provider'] = $this->mediaAdapter->provider();

        return $release;
    }

    /**
     * @param array<int,mixed> $packages
     * @return array<int,array<string,mixed>>
     */
    private function normalizeVariationPackages(Product $product, array $packages): array
    {
        $normalized = [];
        $seenVariationIds = [];

        foreach ($packages as $row) {
            if (!is_array($row)) {
                continue;
            }

            $variation = $this->resolveProductVariation($product, $row);
            if (!$variation) {
                throw new \InvalidArgumentException('Each variation package must reference a valid product variation.');
            }

            if (isset($seenVariationIds[(int) $variation->id])) {
                throw new \InvalidArgumentException('Duplicate variation package entry detected for the same variation.');
            }
            $seenVariationIds[(int) $variation->id] = true;

            $media = $this->mediaAdapter->normalizeReference([
                'attachment_id' => isset($row['attachment_id']) ? absint($row['attachment_id']) : absint($row['media_attachment_id'] ?? 0),
                'download_name' => sanitize_file_name((string) ($row['download_name'] ?? '')),
            ]);

            $packageSdkMeta = [];
            if (isset($row['sdk_meta']) && is_array($row['sdk_meta'])) {
                $packageSdkMeta = $this->sanitizeSdkMeta($row['sdk_meta']);
            }

            $normalized[] = [
                'variation_id' => (int) $variation->id,
                'variation_uuid' => (string) $variation->uuid,
                'variation_name' => sanitize_text_field((string) $variation->name),
                'provider_id' => (int) ($variation->provider_id ?? 0),
                'media' => $media,
                'sdk_meta' => $packageSdkMeta,
                'woo_asset' => isset($row['woo_asset']) && is_array($row['woo_asset'])
                    ? $this->sanitizeScalarMap($row['woo_asset'])
                    : [],
            ];
        }

        usort($normalized, static function (array $a, array $b): int {
            return ((int) $a['variation_id']) <=> ((int) $b['variation_id']);
        });

        return array_values($normalized);
    }

    private function assertAllProductVariationsHavePackages(Product $product, array $packages): void
    {
        $requiredVariationIds = [];
        foreach ($product->variations as $variation) {
            $requiredVariationIds[(int) $variation->id] = true;
        }

        foreach ($packages as $package) {
            if (!is_array($package)) {
                continue;
            }
            unset($requiredVariationIds[(int) ($package['variation_id'] ?? 0)]);
        }

        if (!empty($requiredVariationIds)) {
            throw new \InvalidArgumentException('Published releases require a package file for each product variation.');
        }
    }

    private function resolveProductVariation(Product $product, array $row): ?ProductVariation
    {
        $variationId = absint($row['variation_id'] ?? 0);
        $variationUuid = sanitize_text_field((string) ($row['variation_uuid'] ?? ''));

        foreach ($product->variations as $variation) {
            if ($variationId > 0 && (int) $variation->id === $variationId) {
                return $variation;
            }
            if ($variationUuid !== '' && (string) $variation->uuid === $variationUuid) {
                return $variation;
            }
        }

        return null;
    }

    /**
     * @param array<string,mixed> $sdkMeta
     * @return array<string,mixed>
     */
    private function sanitizeSdkMeta(array $sdkMeta): array
    {
        $sections = [];
        if (isset($sdkMeta['sections']) && is_array($sdkMeta['sections'])) {
            foreach ($sdkMeta['sections'] as $key => $value) {
                $sectionKey = sanitize_key((string) $key);
                if ($sectionKey === '') {
                    continue;
                }
                $sections[$sectionKey] = wp_kses_post((string) $value);
            }
        }

        $icons = [];
        if (isset($sdkMeta['icons']) && is_array($sdkMeta['icons'])) {
            foreach ($sdkMeta['icons'] as $key => $value) {
                $iconKey = sanitize_key((string) $key);
                $iconUrl = esc_url_raw((string) $value);
                if ($iconKey !== '' && $iconUrl !== '') {
                    $icons[$iconKey] = $iconUrl;
                }
            }
        }

        $banners = [];
        if (isset($sdkMeta['banners']) && is_array($sdkMeta['banners'])) {
            foreach ($sdkMeta['banners'] as $key => $value) {
                $bannerKey = sanitize_key((string) $key);
                $bannerUrl = esc_url_raw((string) $value);
                if ($bannerKey !== '' && $bannerUrl !== '') {
                    $banners[$bannerKey] = $bannerUrl;
                }
            }
        }

        $compatibility = [];
        if (isset($sdkMeta['compatibility']) && is_array($sdkMeta['compatibility'])) {
            foreach ($sdkMeta['compatibility'] as $key => $value) {
                $compatibility[sanitize_text_field((string) $key)] = $this->sanitizeRecursive($value);
            }
        }

        $extra = [];
        if (isset($sdkMeta['extra']) && is_array($sdkMeta['extra'])) {
            $extra = $this->sanitizeRecursive($sdkMeta['extra']);
            if (!is_array($extra)) {
                $extra = [];
            }
        }

        return [
            'slug' => sanitize_title((string) ($sdkMeta['slug'] ?? '')),
            'plugin' => sanitize_text_field((string) ($sdkMeta['plugin'] ?? '')),
            'homepage' => esc_url_raw((string) ($sdkMeta['homepage'] ?? '')),
            'url' => esc_url_raw((string) ($sdkMeta['url'] ?? '')),
            'requires' => sanitize_text_field((string) ($sdkMeta['requires'] ?? '')),
            'tested' => sanitize_text_field((string) ($sdkMeta['tested'] ?? '')),
            'requires_php' => sanitize_text_field((string) ($sdkMeta['requires_php'] ?? '')),
            'last_updated' => sanitize_text_field((string) ($sdkMeta['last_updated'] ?? '')),
            'upgrade_notice' => sanitize_textarea_field((string) ($sdkMeta['upgrade_notice'] ?? '')),
            'sections' => $sections,
            'icons' => $icons,
            'banners' => $banners,
            'compatibility' => $compatibility,
            'extra' => $extra,
        ];
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private function sanitizeRecursive($value)
    {
        if (is_array($value)) {
            $result = [];
            foreach ($value as $key => $nestedValue) {
                $cleanKey = is_string($key) ? sanitize_text_field($key) : $key;
                $result[$cleanKey] = $this->sanitizeRecursive($nestedValue);
            }
            return $result;
        }

        if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
            return $value;
        }

        return sanitize_text_field((string) $value);
    }

    /**
     * @param array<string,mixed> $map
     * @return array<string,mixed>
     */
    private function sanitizeScalarMap(array $map): array
    {
        $clean = [];
        foreach ($map as $key => $value) {
            $cleanKey = sanitize_text_field((string) $key);
            if ($cleanKey === '') {
                continue;
            }

            if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
                $clean[$cleanKey] = $value;
                continue;
            }

            if (is_array($value)) {
                $clean[$cleanKey] = $this->sanitizeRecursive($value);
                continue;
            }

            $clean[$cleanKey] = sanitize_text_field((string) $value);
        }

        return $clean;
    }

    private function sanitizeStatus(string $status): string
    {
        $status = strtolower(trim($status));
        $allowed = [static::STATUS_DRAFT, static::STATUS_PUBLISHED, static::STATUS_ARCHIVED];

        return in_array($status, $allowed, true) ? $status : static::STATUS_DRAFT;
    }

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

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

    private function normalizeDate(string $date): string
    {
        $date = trim($date);
        if ($date === '') {
            return gmdate('Y-m-d');
        }

        $ts = strtotime($date);
        if ($ts === false) {
            throw new \InvalidArgumentException('Invalid release_date value.');
        }

        return gmdate('Y-m-d', $ts);
    }

    /**
     * @param mixed $value
     */
    private function toBool($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return ((int) $value) === 1;
        }

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

        return !empty($value);
    }

    private function releaseMetaKey(string $releaseUuid): string
    {
        return static::META_KEY_PREFIX . sanitize_text_field($releaseUuid);
    }

    /**
     * @param array<string,mixed> $release
     */
    private function persistReleaseRecord(int $productId, array $release): void
    {
        $metaKey = $this->releaseMetaKey((string) $release['uuid']);
        $serialized = wp_json_encode($release);
        if (!is_string($serialized) || $serialized === '') {
            throw new \RuntimeException('Failed to serialize release data.');
        }

        $row = Meta::where('entity_type', static::META_ENTITY_TYPE)
            ->where('entity_id', $productId)
            ->where('meta_key', $metaKey)
            ->first();

        if ($row) {
            // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Eloquent model attribute.
            $row->update([
                'meta_value' => $serialized,
            ]);
            // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            return;
        }

        // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Eloquent model attributes.
        Meta::create([
            'entity_type' => static::META_ENTITY_TYPE,
            'entity_id' => $productId,
            'meta_key' => $metaKey,
            'meta_value' => $serialized,
        ]);
        // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
    }

    /**
     * @return array<string,mixed>
     */
    private function decodeReleaseMetaValue(string $value): array
    {
        $decoded = json_decode($value, true);
        return is_array($decoded) ? $decoded : [];
    }

    private function assertUniqueVersionForChannel(int $productId, string $version, string $channel, ?string $ignoreReleaseUuid): void
    {
        $records = $this->loadAllReleaseRecords($productId);
        foreach ($records as $record) {
            if ($ignoreReleaseUuid !== null && (string) ($record['uuid'] ?? '') === $ignoreReleaseUuid) {
                continue;
            }

            if ((string) ($record['version'] ?? '') === $version && (string) ($record['channel'] ?? static::CHANNEL_STABLE) === $channel) {
                throw new \InvalidArgumentException('A release with the same version already exists for this channel.');
            }
        }
    }

    /**
     * @param array<string,mixed> $release
     * @return array<int,string>
     */
    private function syncReleaseToWooAssetsIfNeeded(Product $product, array &$release): array
    {
        $warnings = [];

        if (!ProviderAliasService::isWoo((string) ($product->provider ?? ''))) {
            return $warnings;
        }

        if ((string) ($release['status'] ?? '') !== static::STATUS_PUBLISHED || !$this->toBool($release['is_available'] ?? true)) {
            return $warnings;
        }

        if (!function_exists('wc_get_product') || !class_exists('WC_Product_Download')) {
            $warnings[] = 'WooCommerce downloadable file API is not available; skipped Woo asset sync.';
            return $warnings;
        }

        foreach ($product->variations as $variation) {
            $package = $this->getVariationPackage($release, $variation);
            if (!$package) {
                continue;
            }

            try {
                $wooAsset = $this->syncVariationPackageToWoo($product, $variation, $release, $package);
                if ($wooAsset) {
                    foreach ($release['variation_packages'] as $index => $candidate) {
                        if (!is_array($candidate)) {
                            continue;
                        }
                        if ((int) ($candidate['variation_id'] ?? 0) === (int) $variation->id) {
                            $release['variation_packages'][$index]['woo_asset'] = $wooAsset;
                            break;
                        }
                    }
                }
            } catch (\Throwable $e) {
                $warnings[] = sprintf(
                    'Woo asset sync failed for variation %s: %s',
                    (string) $variation->name,
                    $e->getMessage()
                );
            }
        }

        return array_values(array_unique($warnings));
    }

    /**
     * @param array<string,mixed> $release
     * @param array<string,mixed> $package
     * @return array<string,mixed>|null
     */
    private function syncVariationPackageToWoo(Product $product, ProductVariation $variation, array $release, array $package): ?array
    {
        $wooParentProductId = absint($product->provider_id ?? 0);
        if ($wooParentProductId <= 0) {
            throw new \RuntimeException('Mapped WooCommerce product ID is missing.');
        }

        $wooVariationId = absint($variation->provider_id ?? 0);
        $targetProductId = $wooVariationId > 0 ? $wooVariationId : $wooParentProductId;

        $wcProduct = wc_get_product($targetProductId);
        if (!$wcProduct) {
            throw new \RuntimeException('Mapped WooCommerce product/variation could not be loaded.');
        }

        $media = is_array($package['media'] ?? null) ? $package['media'] : [];
        $fileUrl = $this->mediaAdapter->resolvePublicUrl($media);

        $downloadId = $this->buildWooDownloadId((string) ($release['channel'] ?? static::CHANNEL_STABLE), (string) ($variation->uuid ?? ''));
        $downloadName = sanitize_text_field((string) ($media['download_name'] ?? ''));
        if ($downloadName === '') {
            $downloadName = sanitize_text_field(trim((string) $product->name . ' ' . (string) $variation->name . ' ' . (string) ($release['version'] ?? '')));
        }

        $downloads = $wcProduct->get_downloads();
        if (!is_array($downloads)) {
            $downloads = [];
        }

        $download = new \WC_Product_Download();
        $download->set_id($downloadId);
        $download->set_name($downloadName);
        $download->set_file($fileUrl);

        $downloads[$downloadId] = $download;
        $wcProduct->set_downloadable(true);
        $wcProduct->set_downloads($downloads);
        $wcProduct->save();

        return [
            'download_id' => $downloadId,
            'download_name' => $downloadName,
            'file' => $fileUrl,
            'woo_product_id' => $wooParentProductId,
            'woo_variation_id' => $wooVariationId > 0 ? $wooVariationId : null,
            'target_product_id' => $targetProductId,
            'synced_at' => gmdate('Y-m-d H:i:s'),
        ];
    }

    private function buildWooDownloadId(string $channel, string $variationUuid): string
    {
        $channel = sanitize_key($channel);
        $variationUuid = sanitize_key(str_replace('-', '_', $variationUuid));

        return sprintf('licenzo_release_%s_%s', $channel ?: 'stable', $variationUuid ?: 'variation');
    }
}
