<?php
/**
 * Content writes and conversational add* shortcuts.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

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

/**
 * Executes B10 content and shortcut abilities by composing B8 and B9.
 */
class ContentShortcutHandler
{
    /**
     * @var array<string, string>
     */
    private const SHORTCUTS = [
        'addSection'   => 'section',
        'addContainer' => 'container',
        'addDiv'       => 'div',
        'addHeading'   => 'heading',
        'addText'      => 'text',
        'addImage'     => 'image',
        'addButton'    => 'button',
    ];

    public function __construct(
        private readonly ?CompositionHandler $composition = null,
        private readonly ?ElementStyleHandler $styles = null,
        private readonly ?ElementGateway $gateway = null,
        private readonly ?ControlIntrospector $introspector = null,
        private readonly ?SystemVocabulary $vocabulary = null,
        private readonly ?TreeSummarizer $summarizer = null,
        private readonly ?ElementContentMapper $mapper = null
    ) {
    }

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

        $items = $this->contentItems($params);
        if ($items === []) {
            return $this->failure(
                'I need the elements and their new content. Which ones should I update?',
                'missing_content'
            );
        }

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

        /** @var ElementTree $tree */
        $tree         = $loaded['tree'];
        $list         = $tree->toList();
        $elementIds = [];
        $notes      = [];

        foreach ($items as $item) {
            $elementId = (string) ($item['element_id'] ?? '');
            $content   = is_array($item['content'] ?? null) ? $item['content'] : [];
            $mapped    = $this->mapper()->toSettings('', $content);
            if (($mapped['success'] ?? false) !== true) {
                return $mapped;
            }

            $settings = $mapped['settings'];
            if ($settings === []) {
                return $this->failure(
                    'I need the elements and their new content. Which ones should I update?',
                    'missing_content'
                );
            }

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

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

                $cssGuard = CssTextGuard::validateSettings($settings);
                if ($cssGuard !== null) {
                    return $cssGuard;
                }

                $current                    = is_array($element['settings'] ?? null) ? $element['settings'] : [];
                $list[$index]['settings']   = array_merge($current, $settings);
                $elementIds[] = $elementId;
                $notes        = array_merge($notes, $this->vocabulary()->notesBeforeStyle($settings));
                $found                      = true;
                break;
            }

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

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            ElementTree::fromList($list),
            $dryRun,
            'Content preview is ready.',
            'I updated the content. Refresh the front to see it.',
            $elementIds,
            $notes
        );
    }

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

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

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

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

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

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

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

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

        $name    = self::SHORTCUTS[$method] ?? '';
        $guarded = $this->mapper()->shortcutContentGuard($name, $params);
        if ($guarded !== null) {
            return $guarded;
        }

        $content = is_array($params['content'] ?? null) ? $params['content'] : [];
        $mapped  = $this->mapper()->toSettings($name, $content);
        if (($mapped['success'] ?? false) !== true) {
            return $mapped;
        }

        $styles = is_array($params['styles'] ?? null) ? $params['styles'] : [];
        if ($styles !== []) {
            $invalid = $this->introspector()->validateSettings($name, $styles, true);
            if ($invalid !== null) {
                return $invalid;
            }
        }

        $notes = $styles === [] ? [] : $this->vocabulary()->notesBeforeStyle($styles);

        $inserted = $this->composition()->insertElement(
            [
                'post_id'      => $params['post_id'] ?? $params['postId'] ?? 0,
                'area'         => $params['area'] ?? BricksMetaKeys::AREA_CONTENT,
                'parent_id'    => $params['parent_id'] ?? '',
                'position'     => $params['position'] ?? 'child',
                'reference_id' => $params['reference_id'] ?? '',
                'name'         => $name,
                'settings'     => array_merge($mapped['settings'], $styles),
            ],
            $dryRun
        );

        if (($inserted['success'] ?? false) !== true) {
            return $inserted;
        }

        $elementId = (string) ($inserted['element_id'] ?? '');
        $classes   = $this->stringList($params['classes'] ?? []);
        $result    = $inserted;

        if ($classes !== [] && $elementId !== '') {
            $applied = $this->styles()->applyClasses(
                [
                    'post_id'     => $params['post_id'] ?? $params['postId'] ?? 0,
                    'area'        => $params['area'] ?? BricksMetaKeys::AREA_CONTENT,
                    'element_ids' => [$elementId],
                    'classes'     => $classes,
                ],
                $dryRun
            );

            if (($applied['success'] ?? false) === true) {
                $result = array_merge($result, [
                    'element_ids'      => $applied['element_ids'] ?? [$elementId],
                    'vocabulary_notes' => $applied['vocabulary_notes'] ?? [],
                ]);
            } elseif (!$dryRun || ($applied['error'] ?? '') !== 'element_not_found') {
                return $applied;
            }
        }

        if ($styles !== [] && $elementId !== '') {
            $styled = $this->styles()->styleElement(
                [
                    'post_id'    => $params['post_id'] ?? $params['postId'] ?? 0,
                    'area'       => $params['area'] ?? BricksMetaKeys::AREA_CONTENT,
                    'element_id' => $elementId,
                    'styles'     => $styles,
                ],
                $dryRun
            );

            if (($styled['success'] ?? false) === true) {
                $notes = array_merge($notes, is_array($styled['vocabulary_notes'] ?? null) ? $styled['vocabulary_notes'] : []);
            } elseif (!$dryRun || ($styled['error'] ?? '') !== 'element_not_found') {
                return $styled;
            }
        }

        $label = $this->shortcutLabel($name);

        return array_merge($result, [
            'message'          => $dryRun
                ? 'Add preview is ready.'
                : sprintf('I added the %s. Refresh the front to see it.', $label),
            'name'             => $name,
            'element_id'       => $elementId,
            'vocabulary_notes' => $notes,
        ]);
    }

    /**
     * @param array<string, mixed> $params
     * @return array<int, array<string, mixed>>
     */
    private function contentItems(array $params): array
    {
        if (isset($params['items']) && is_array($params['items'])) {
            $items = [];
            foreach ($params['items'] as $item) {
                if (is_array($item)) {
                    $items[] = $item;
                }
            }

            return $items;
        }

        $elementId = (string) ($params['element_id'] ?? '');
        $content   = is_array($params['content'] ?? null) ? $params['content'] : [];
        if ($elementId === '' || $content === []) {
            return [];
        }

        return [[
            'element_id' => $elementId,
            'content'    => $content,
        ]];
    }

    /**
     * @param string[] $elementIds
     * @param array<int, array<string, mixed>> $notes
     * @return array<string, mixed>
     */
    private function persist(
        int $postId,
        string $area,
        ElementTree $tree,
        bool $dryRun,
        string $previewMessage,
        string $doneMessage,
        array $elementIds,
        array $notes
    ): array {
        $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> $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 content.', '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;
    }

    private function shortcutLabel(string $name): string
    {
        return match ($name) {
            'section'   => 'section',
            'container' => 'container',
            'div'       => 'div',
            'heading'   => 'heading',
            'text'      => 'text',
            'image'     => 'image',
            'button'    => 'button',
            default     => 'element',
        };
    }

    /**
     * @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 composition(): CompositionHandler
    {
        return $this->composition ?? new CompositionHandler();
    }

    private function styles(): ElementStyleHandler
    {
        return $this->styles ?? new ElementStyleHandler();
    }

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

    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();
    }

    private function mapper(): ElementContentMapper
    {
        return $this->mapper ?? new ElementContentMapper();
    }
}
