<?php
/**
 * Element style and class writes with hostile key validation.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\BricksEnv;
use LolaCoreForBricks\Elements\ElementTree;
use LolaCoreForBricks\Elements\TreeSummarizer;
use LolaCoreForBricks\Gateways\ClassGateway;
use LolaCoreForBricks\Gateways\ElementGateway;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\ControlIntrospector;
use LolaCoreForBricks\Support\CssTextGuard;
use LolaCoreForBricks\Support\SettingsKeyGrammar;
use LolaCoreForBricks\Support\SystemVocabulary;

/**
 * Executes B9 element styling abilities.
 */
class ElementStyleHandler
{
    public function __construct(
        private readonly ?ElementGateway $gateway = null,
        private readonly ?ClassGateway $classes = null,
        private readonly ?ControlIntrospector $introspector = null,
        private readonly ?SystemVocabulary $vocabulary = null,
        private readonly ?TreeSummarizer $summarizer = null
    ) {
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function styleElement(array $params, bool $dryRun = false): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $styles = is_array($params['styles'] ?? null) ? $params['styles'] : [];
        if ($styles === []) {
            return $this->failure('I need at least one style to apply. Which setting should I change?', 'missing_styles');
        }

        $composed = $this->composeStyles(
            $styles,
            isset($params['breakpoint']) ? (string) $params['breakpoint'] : null,
            isset($params['state']) ? (string) $params['state'] : null
        );

        $loaded = $this->loadTree($params);
        if (isset($loaded['error'])) {
            return $loaded;
        }

        $elementId = (string) ($params['element_id'] ?? '');
        $element   = $loaded['tree']->findById($elementId);
        if ($element === null) {
            return $this->failure('I could not find that element on this page.', 'element_not_found');
        }

        $invalid = $this->introspector()->validateSettings(
            (string) ($element['name'] ?? ''),
            $composed,
            true
        );
        if ($invalid !== null) {
            return $invalid;
        }

        return $this->patchSettings($loaded, $elementId, $composed, $dryRun, [$elementId], 'Style preview is ready.', 'I updated the element style. Refresh the front to see it.');
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function applyClasses(array $params, bool $dryRun = false): array
    {
        return $this->changeClasses($params, $dryRun, true);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function removeClasses(array $params, bool $dryRun = false): array
    {
        return $this->changeClasses($params, $dryRun, false);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function updateElementSettings(array $params, bool $dryRun = false): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $settings = is_array($params['settings'] ?? null) ? $params['settings'] : [];
        if ($settings === []) {
            return $this->failure('I need at least one setting to update. Which one?', 'missing_settings');
        }

        $settings = $this->normalizeKeys($settings);
        $loaded   = $this->loadTree($params);
        if (isset($loaded['error'])) {
            return $loaded;
        }

        $elementId = (string) ($params['element_id'] ?? '');
        $element   = $loaded['tree']->findById($elementId);
        if ($element === null) {
            return $this->failure('I could not find that element on this page.', 'element_not_found');
        }

        $invalid = $this->introspector()->validateSettings((string) ($element['name'] ?? ''), $settings);
        if ($invalid !== null) {
            return $invalid;
        }

        return $this->patchSettings(
            $loaded,
            $elementId,
            $settings,
            $dryRun,
            [$elementId],
            'Settings preview is ready.',
            'I updated the element settings. Refresh the front to see it.'
        );
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function changeClasses(array $params, bool $dryRun, bool $apply): array
    {
        $guard = $this->environmentGuard($params);
        if ($guard !== null) {
            return $guard;
        }

        $elementIds = $this->stringList($params['element_ids'] ?? $params['element_id'] ?? []);
        $classRefs  = $this->stringList($params['classes'] ?? []);
        if ($elementIds === [] || $classRefs === []) {
            return $this->failure(
                'I need the elements and the classes to do that.',
                'missing_targets'
            );
        }

        $resolved = [];
        foreach ($classRefs as $ref) {
            $class = $this->classes()->resolveClassRef($ref);
            if ($class === null) {
                return $this->failure(
                    'I could not find that global class. Check the name and try again.',
                    'class_not_found'
                );
            }

            $resolved[$class['id']] = $class['name'];
        }

        $loaded = $this->loadTree($params);
        if (isset($loaded['error'])) {
            return $loaded;
        }

        /** @var ElementTree $tree */
        $tree = $loaded['tree'];
        $list = $tree->toList();
        $classIds = array_keys($resolved);

        foreach ($elementIds as $elementId) {
            $found = false;
            foreach ($list as $index => $element) {
                if ((string) ($element['id'] ?? '') !== $elementId) {
                    continue;
                }

                $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
                $current  = $this->stringList($settings['_cssGlobalClasses'] ?? []);
                $settings['_cssGlobalClasses'] = $apply
                    ? array_values(array_unique(array_merge($current, $classIds)))
                    : array_values(array_filter($current, static fn (string $id): bool => !in_array($id, $classIds, true)));
                $list[$index]['settings'] = $settings;
                $found = true;
                break;
            }

            if (!$found) {
                return $this->failure('I could not find that element on this page.', 'element_not_found');
            }
        }

        $preview = $apply ? 'Class preview is ready.' : 'Remove-class preview is ready.';
        $done    = $apply
            ? 'I applied the classes. Refresh the front to see them.'
            : 'I removed the classes. Refresh the front to see the change.';

        return $this->persist($loaded['post_id'], $loaded['area'], ElementTree::fromList($list), $dryRun, $preview, $done, $elementIds, []);
    }

    /**
     * @param array<string, mixed> $loaded
     * @param array<string, mixed> $incoming
     * @param string[]             $elementIds
     * @return array<string, mixed>
     */
    private function patchSettings(
        array $loaded,
        string $elementId,
        array $incoming,
        bool $dryRun,
        array $elementIds,
        string $preview,
        string $done
    ): array {
        $cssGuard = CssTextGuard::validateSettings($incoming);
        if ($cssGuard !== null) {
            return $cssGuard;
        }

        /** @var ElementTree $tree */
        $tree = $loaded['tree'];
        $list = $tree->toList();
        foreach ($list as $index => $element) {
            if ((string) ($element['id'] ?? '') !== $elementId) {
                continue;
            }

            $settings = is_array($element['settings'] ?? null) ? $element['settings'] : [];
            $list[$index]['settings'] = array_merge($settings, $incoming);
            break;
        }

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            ElementTree::fromList($list),
            $dryRun,
            $preview,
            $done,
            $elementIds,
            $incoming
        );
    }

    /**
     * @param string[]             $elementIds
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function persist(
        int $postId,
        string $area,
        ElementTree $tree,
        bool $dryRun,
        string $previewMessage,
        string $doneMessage,
        array $elementIds,
        array $settings
    ): array {
        $notes   = $this->vocabulary()->notesBeforeStyle($settings);
        $written = $this->gateway()->write($postId, $area, $tree->toList(), $dryRun);
        if (($written['success'] ?? false) !== true) {
            return $written;
        }

        $summary = $this->summarizer()->summarize($tree->toList(), 1);

        return array_merge($written, [
            'message'          => $dryRun ? $previewMessage : $doneMessage,
            'element_ids'      => $elementIds,
            'vocabulary_notes' => $notes,
            'impact'           => [
                'pages'      => 1,
                'elements'   => count($elementIds),
                'page_names' => [get_the_title($postId)],
            ],
            'summary'          => $summary['nodes'],
            'totals'           => $summary['totals'],
        ]);
    }

    /**
     * @param array<string, mixed> $styles
     * @return array<string, mixed>
     */
    private function composeStyles(array $styles, ?string $breakpoint, ?string $state): array
    {
        $composed = [];
        foreach ($styles as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            $parsed = SettingsKeyGrammar::parse($key);
            $composed[SettingsKeyGrammar::compose(
                $parsed['base'],
                $parsed['breakpoint'] ?? $breakpoint,
                $parsed['state'] ?? $state,
                $parsed['variant']
            )] = $value;
        }

        return $composed;
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function normalizeKeys(array $settings): array
    {
        $normalized = [];
        foreach ($settings as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            $parsed = SettingsKeyGrammar::parse($key);
            if ($parsed['legacy']) {
                $key = SettingsKeyGrammar::compose(
                    $parsed['base'],
                    $parsed['breakpoint'],
                    $parsed['state'],
                    $parsed['variant']
                );
            }

            $normalized[$key] = $value;
        }

        return $normalized;
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function loadTree(array $params): array
    {
        $postId = absint($params['post_id'] ?? $params['postId'] ?? 0);
        $area   = $this->gateway()->resolveArea((string) ($params['area'] ?? BricksMetaKeys::AREA_CONTENT));
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->failure('I could not find that page. It may have been deleted.', 'post_not_found');
        }

        return [
            'post_id' => $postId,
            'area'    => $area,
            'tree'    => ElementTree::fromList($this->gateway()->read($postId, $area)),
        ];
    }

    /**
     * @param mixed $value
     * @return string[]
     */
    private function stringList(mixed $value): array
    {
        if (is_string($value) && $value !== '') {
            return [$value];
        }

        if (!is_array($value)) {
            return [];
        }

        $items = [];
        foreach ($value as $item) {
            $item = trim((string) $item);
            if ($item !== '') {
                $items[] = $item;
            }
        }

        return $items;
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>|null
     */
    private function environmentGuard(array $params): ?array
    {
        if ($rejection = BricksCodeGuard::rejectionResponse($params)) {
            return $rejection;
        }

        if (!BricksEnv::userCanReadDesignSystem()) {
            return $this->failure('I need administrator access to change Bricks page styles.', 'forbidden');
        }

        if (!BricksEnv::isAvailable() || !BricksEnv::meetsFloor()) {
            return $this->failure(
                'Bricks is not available on this site yet. Activate Bricks '
                . BricksEnv::FLOOR_VERSION . ' or newer, then try again.',
                'bricks_unavailable'
            );
        }

        return null;
    }

    /**
     * @return array{success: false, message: string, error: string}
     */
    private function failure(string $message, string $error): array
    {
        return [
            'success' => false,
            'message' => $message,
            'error'   => $error,
        ];
    }

    private function gateway(): ElementGateway
    {
        return $this->gateway ?? new ElementGateway();
    }

    private function classes(): ClassGateway
    {
        return $this->classes ?? new ClassGateway();
    }

    private function introspector(): ControlIntrospector
    {
        return $this->introspector ?? new ControlIntrospector();
    }

    private function vocabulary(): SystemVocabulary
    {
        return $this->vocabulary ?? new SystemVocabulary();
    }

    private function summarizer(): TreeSummarizer
    {
        return $this->summarizer ?? new TreeSummarizer();
    }
}
