<?php

/**
 * Deterministic normalization applied to generated plugin code at the
 * write-to-disk boundary (local install + Playground blueprint).
 *
 * @package LolaCore\PluginBuilder\Packaging
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Packaging;

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

/**
 * Enforces two contracts the LLM cannot be trusted to honor by itself, so they
 * are repaired deterministically as the code is packaged onto disk:
 *
 *  1. Asset references must point at {slug}.css / {slug}.js. The packaging always
 *     writes the files as {slug}.ext, but the model frequently enqueues a
 *     shorthand name (e.g. plugins_url('style.css', __FILE__)) which 404s and
 *     leaves the plugin unstyled. We rewrite the filename to match the slug.
 *
 *  2. The plugin header Version must reflect the library version, not the static
 *     "1.0.0" the model emits on every iteration — otherwise WordPress shows the
 *     same version after every update and the user cannot tell what is live.
 *
 * Pure string transforms (no WordPress calls), idempotent, so they can run on
 * already-correct code without changing it and repair legacy library entries on
 * their way to disk.
 */
final class PluginFileNormalizer {

	/**
	 * Apply every disk-write normalization to the main PHP file.
	 */
	public static function normalizePhp( string $php, string $slug, int $version ): string {
		$php = self::normalizeAssetReferences($php, $slug);
		$php = self::rewriteHeaderVersion($php, $version);
		$php = self::ensureWooCommerceSettingsPageBase($php);

		return $php;
	}

	/**
	 * Inject a WC_Settings_Page require before the first settings-page subclass.
	 *
	 * WooCommerce only autoloads that admin base during wp-admin bootstrap. Generated
	 * plugins often declare `extends WC_Settings_Page` at file scope, which fatals on
	 * activation (admin-ajax) and on the front end unless the base file is required first.
	 */
	public static function ensureWooCommerceSettingsPageBase( string $php ): string {
		if (! str_contains($php, 'extends WC_Settings_Page')) {
			return $php;
		}

		if (str_contains($php, 'class-wc-settings-page.php')) {
			return $php;
		}

		$snippet  = "if (defined('WC_ABSPATH') && ! class_exists('WC_Settings_Page', false)) {\n";
		$snippet .= "\trequire_once WC_ABSPATH . 'includes/admin/settings/class-wc-settings-page.php';\n";
		$snippet .= "}\n";

		$replaced = preg_replace(
			'/^(\s*class\s+\w+\s+extends\s+WC_Settings_Page\b)/m',
			$snippet . '$1',
			$php,
			1,
			$count
		);

		return ( $count > 0 && is_string($replaced) ) ? $replaced : $php;
	}

	/**
	 * Rewrite enqueued asset filenames to {slug}.css / {slug}.js.
	 *
	 * Targets quoted, root-level filenames ending in .css/.js (no slash, so
	 * subdirectory paths and unrelated strings are left untouched). Already-correct
	 * references are rewritten to the same value, so the transform is idempotent.
	 */
	public static function normalizeAssetReferences( string $php, string $slug ): string {
		$slug = self::sanitizeSlug($slug);
		if ($slug === '') {
			return $php;
		}

		$pattern = '/([\'"])([A-Za-z0-9_-]+)\.(css|js)\1/';

		$result = preg_replace_callback(
			$pattern,
			static function ( array $m ) use ( $slug ): string {
				$quote = $m[1];
				$ext   = strtolower($m[3]);

				return $quote . $slug . '.' . $ext . $quote;
			},
			$php
		);

		return is_string($result) ? $result : $php;
	}

	/**
	 * Force the plugin header `Version:` to the library version (N.0.0).
	 *
	 * The user thinks in v1/v2/v3 (the chat says "saved (v2)"), so the header maps
	 * directly: version N becomes "N.0.0". If the header has a Version line it is
	 * replaced; if it has a plugin header but no Version line, one is injected
	 * after the Plugin Name line; otherwise the PHP is returned unchanged.
	 */
	public static function rewriteHeaderVersion( string $php, int $version ): string {
		$version = max(1, $version);
		$value   = $version . '.0.0';

		$replaced = preg_replace(
			'/^(\s*\*\s*Version:\s*).*$/mi',
			'${1}' . $value,
			$php,
			1,
			$count
		);

		if (is_string($replaced) && $count > 0) {
			return $replaced;
		}

		// No Version line — inject one right after the Plugin Name header line.
		$injected = preg_replace(
			'/^(\s*\*\s*Plugin Name:.*$)/mi',
			"$1\n * Version: " . $value,
			$php,
			1,
			$nameCount
		);

		if (is_string($injected) && $nameCount > 0) {
			return $injected;
		}

		return $php;
	}

	/**
	 * Minimal slug sanitizer usable without WordPress loaded (unit tests).
	 *
	 * Mirrors sanitize_title for the kebab-case slugs the builder produces:
	 * lowercase, alphanumerics and hyphens only, collapsed and trimmed.
	 */
	private static function sanitizeSlug( string $slug ): string {
		if (function_exists('sanitize_title')) {
			return sanitize_title($slug);
		}

		$slug = strtolower(trim($slug));
		$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';

		return trim($slug, '-');
	}
}
