<?php
/**
 * Vite integration for WordPress
 *
 * @package ViteForWp
 */

declare(strict_types=1);

namespace FleekDash\Libs\Assets;

require_once __DIR__ . '/AssetManifestException.php';

const VITE_CLIENT_SCRIPT_HANDLE = 'vite-client';

const MANIFEST_JSON_FILENAME = 'manifest.json';

const ASSETS_DIRECTORY_NAME = 'assets';

require_once __DIR__ . '/asset-environment.php';

/**
 * Get manifest data
 *
 * @since 0.1.0
 * @since 0.8.0 Use wp_json_file_decode().
 *
 * @param string $manifest_dir Path to manifest directory.
 *
 * @throws AssetManifestException When the file doesn't exist, is unreadable, or contains invalid data.
 *
 * @return object Object containing manifest type and data.
 */
function getManifest(string $manifest_dir): object
{
    $dev_manifest = 'vite-dev-server';
    static $manifests = array();
    $is_dev = false;

    $manifest_dir = wp_normalize_path($manifest_dir);
    $manifest_dir = rtrim($manifest_dir, '/\\');

    $file_names = array($dev_manifest, 'manifest');

    foreach ($file_names as $file_name) {
        $is_dev = $file_name === $dev_manifest;
        if ($is_dev && (!isLocalDevelopmentHost() || !isDevelopmentEnvironment())) {
            continue;
        }

        $manifest_path = $manifest_dir . DIRECTORY_SEPARATOR . $file_name . '.json';
        $manifest_path = wp_normalize_path($manifest_path);

        if (isset($manifests[$manifest_path])) {
            return $manifests[$manifest_path];
        }

        if (is_file($manifest_path) && is_readable($manifest_path)) {
            break;
        }

        unset($manifest_path);
    }

    if (!isset($manifest_path)) {
        $is_dev = isDevelopmentEnvironment();
        if ($is_dev || (defined('WP_DEBUG') && WP_DEBUG)) {
            throw new AssetManifestException(esc_html(sprintf('[Vite] No manifest found in %s.', $manifest_dir)));
        }

        throw new AssetManifestException('No manifest found');
    }

    $manifest = wp_json_file_decode($manifest_path);

    if (!$manifest) {
        throw new AssetManifestException(esc_html(sprintf('[Vite] Failed to read manifest file %s.', $manifest_path)));
    }

    /**
     * Filter manifest data
     *
     * @param array  $manifest      Manifest data.
     * @param string $manifest_dir  Manifest directory path.
     * @param string $manifest_path Manifest file path.
     * @param bool   $is_dev        Whether this is a manifest for development assets.
     */
    $manifest = apply_filters('vite_for_wp__manifest_data', $manifest, $manifest_dir, $manifest_path);

    $manifests[$manifest_path] = (object) array(
        'data' => $manifest,
        'dir' => $manifest_dir,
        'is_dev' => $is_dev,
    );

    return $manifests[$manifest_path];
}

require_once __DIR__ . '/vite-development-assets.php';

/**
 * @param array{scripts: string[], styles: string[]} $assets
 */
function appendProductionCssFromItem(
    object $item,
    string $url,
    array $options,
    array &$assets,
    string $handle_prefix,
): void {
    if (empty($item->css)) {
        return;
    }

    foreach ($item->css as $index => $css_file_path) {
        $style_handle = "{$handle_prefix}-{$index}";
        $style_src = "{$url}/{$css_file_path}";
        if (wp_register_style($style_handle, $style_src, $options['css-dependencies'], null, $options['css-media'])) {
            $assets['styles'][] = $style_handle;
        }
    }
}

/**
 * @param array{scripts: string[], styles: string[]} $assets
 */
function appendProductionCssFromImports(
    object $manifest,
    object $item,
    string $url,
    array $options,
    array &$assets,
    string $base_handle,
): void {
    if (empty($item->imports)) {
        return;
    }

    foreach ($item->imports as $import_key) {
        $import_item = $manifest->data->{$import_key} ?? null;
        if (!$import_item || empty($import_item->css)) {
            continue;
        }

        foreach ($import_item->css as $index => $css_file_path) {
            $style_handle = "{$base_handle}-import-{$import_key}-{$index}";
            $style_src = "{$url}/{$css_file_path}";
            if (wp_register_style($style_handle, $style_src, $options['css-dependencies'], null, $options['css-media'])) {
                $assets['styles'][] = $style_handle;
            }
        }
    }
}

/**
 * @return array{scripts: string[], styles: string[]}|null
 */
function buildProductionAssetsForEntry(
    object $manifest,
    object $item,
    string $entry,
    string $url,
    array $options,
): ?array {
    $assets = array(
        'scripts' => array(),
        'styles' => array(),
    );

    $src = "{$url}/{$item->file}";

    if (!$options['css-only']) {
        filterScriptTag($options['handle']);

        if (registerScriptCompat($options['handle'], $src, $options['dependencies'], null, $options['in_footer'])) {
            $assets['scripts'][] = $options['handle'];
        }
    }

    appendProductionCssFromItem($item, $url, $options, $assets, $options['handle']);
    appendProductionCssFromImports($manifest, $item, $url, $options, $assets, $options['handle']);

    /**
     * Filter registered production assets
     *
     * @param array  $assets   Registered assets.
     * @param object $manifest Manifest object.
     * @param string $entry    Entrypoint file.
     * @param array  $options  Enqueue options.
     */
    return apply_filters('vite_for_wp__production_assets', $assets, $manifest, $entry, $options);
}

/**
 * Load production asset
 *
 * @since 0.1.0
 *
 * @param object $manifest Asset manifest.
 * @param string $entry    Entrypoint to enqueue.
 * @param array  $options  Enqueue options.
 *
 * @return array|null Array containing registered scripts & styles or NULL if there was an error.
 */
function loadProductionAsset(object $manifest, string $entry, array $options): ?array
{
    $url = prepareAssetUrl($manifest->dir);

    if (!isset($manifest->data->{$entry})) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            wp_die(esc_html(sprintf('[Vite] Entry %s not found.', $entry)));
        }

        return null;
    }

    $item = $manifest->data->{$entry};

    if (!$item || !isset($item->file)) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            wp_die(esc_html(sprintf('[Vite] Entry %s has no file property.', $entry)));
        }

        return null;
    }

    return buildProductionAssetsForEntry($manifest, $item, $entry, $url, $options);
}

/**
 * Parse register/enqueue options
 *
 * @since 0.1.0
 *
 * @param array $options Array of options.
 *
 * @return array Array of options merged with defaults.
 */
function parseOptions(array $options): array
{
    $defaults = array(
        'css-dependencies' => array(),
        'css-media' => 'all',
        'css-only' => false,
        'dependencies' => array(),
        'handle' => '',
        'in_footer' => false,
    );
    $parsed = wp_parse_args($options, $defaults);
    if (array_key_exists('in-footer', $parsed) && !array_key_exists('in_footer', $options)) {
        $parsed['in_footer'] = (bool) $parsed['in-footer'];
    }
    unset($parsed['in-footer']);

    return $parsed;
}

/**
 * Register script with normalized WP 7+ args support.
 *
 * @param bool|array<string, mixed> $args
 */
function registerScriptCompat(string $handle, string $src, array $deps, mixed $ver, $args): bool
{
    $normalizedArgs = $args;
    if (is_array($normalizedArgs)) {
        if (array_key_exists('defer', $normalizedArgs) && !array_key_exists('strategy', $normalizedArgs)) {
            $normalizedArgs['strategy'] = $normalizedArgs['defer'] ? 'defer' : null;
        }
        if (array_key_exists('in-footer', $normalizedArgs) && !array_key_exists('in_footer', $normalizedArgs)) {
            $normalizedArgs['in_footer'] = (bool) $normalizedArgs['in-footer'];
        }
        unset($normalizedArgs['defer'], $normalizedArgs['in-footer']);
    }

    return wp_register_script($handle, $src, $deps, $ver, $normalizedArgs);
}

/**
 * Prepare asset url
 *
 * @author Justin Slamka <jslamka5685@gmail.com>
 * @since 0.4.0
 * @since 0.6.1 Normalize paths so they work on Windows as well.
 *
 * @param string $dir Asset directory.
 *
 * @return string
 */
function prepareAssetUrl(string $dir): string
{
    $content_dir = wp_normalize_path(WP_CONTENT_DIR);
    $manifest_dir = wp_normalize_path($dir);
    $url = content_url(str_replace($content_dir, '', $manifest_dir));
    $pattern = '/(?<address>https?:\/\/.*\/)(?<fullPath>wp-content(?<removablePath>\/.*)\/(?:plugins|themes)\/.*)/';
    $url_matches_pattern = preg_match($pattern, $url, $url_parts);

    if ($url_matches_pattern === 0) {
        return $url;
    }

    ['address' => $address, 'fullPath' => $full_path, 'removablePath' => $removable_path] = $url_parts;

    return sprintf('%s%s', $address, str_replace($removable_path, '', $full_path));
}

/**
 * Register asset
 *
 * @since 0.1.0
 *
 * @see loadDevelopmentAsset
 * @see loadProductionAsset
 *
 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
 * @param string $entry        Entrypoint to enqueue.
 * @param array  $options      Enqueue options.
 *
 * @return array|null
 */
function registerAsset(string $manifest_dir, string $entry, array $options): ?array
{
    $is_dev = isDevelopmentEnvironment();

    $manifest_path = $manifest_dir . '/' . MANIFEST_JSON_FILENAME;
    $dev_manifest_path = $manifest_dir . '/' . VITE_DEV_SERVER_MANIFEST_FILENAME;
    $manifest_exists = is_file($manifest_path) && is_readable($manifest_path);
    $dev_manifest_exists = is_file($dev_manifest_path) && is_readable($dev_manifest_path);

    if (!$is_dev && !$manifest_exists && !$dev_manifest_exists) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('FleekDash: No manifest found in production, skipping Vite asset loading');
        }

        return null;
    }

    try {
        $manifest = getManifest($manifest_dir);
    } catch (AssetManifestException $e) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('FleekDash: Could not load manifest: ' . $e->getMessage());
        }

        return null;
    }

    $options = parseOptions($options);

    return $manifest->is_dev
        ? loadDevelopmentAsset($manifest, $entry, $options)
        : loadProductionAsset($manifest, $entry, $options);
}

/**
 * Enqueue asset
 *
 * @since 0.1.0
 *
 * @see registerAsset
 *
 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
 * @param string $entry        Entrypoint to enqueue.
 * @param array  $options      Enqueue options.
 */
function enqueueAsset(string $manifest_dir, string $entry, array $options): bool
{
    $assets = registerAsset($manifest_dir, $entry, $options);

    if (is_null($assets)) {
        return false;
    }

    $map = array(
        'scripts' => 'wp_enqueue_script',
        'styles' => 'wp_enqueue_style',
    );

    foreach ($assets as $group => $handles) {
        $func = $map[$group];

        foreach ($handles as $handle) {
            $func($handle);
        }
    }

    return true;
}

function globalsStylesPublicUrl(string $asset_file): string
{
    return FLEEKDASH_PLUGIN_URL . ASSETS_DIRECTORY_NAME . '/' . $asset_file;
}

require_once __DIR__ . '/direct-entry-assets.php';
require_once __DIR__ . '/stylesheet-manifest-helpers.php';
require_once __DIR__ . '/globals-css-assets.php';
require_once __DIR__ . '/iframe-tokens-assets.php';
