<?php
/**
 * Page composition handlers with dry-run previews.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\BricksEnv;
use LolaCoreForBricks\Elements\ElementTree;
use LolaCoreForBricks\Elements\IdGenerator;
use LolaCoreForBricks\Elements\TreeSummarizer;
use LolaCoreForBricks\Gateways\ElementGateway;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\BricksMetaKeys;

/**
 * Executes B8 composition abilities.
 */
class CompositionHandler
{
    public function __construct(
        private readonly ?ElementGateway $gateway = null,
        private readonly ?IdGenerator $ids = null,
        private readonly ?TreeSummarizer $summarizer = null
    ) {
    }

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

        $postId = $this->postId($params);
        $area   = $this->area($params);
        if ($postId <= 0 || get_post($postId) === null) {
            return $this->failure('I could not find that page. It may have been deleted.', 'post_not_found');
        }

        $elements = $this->gateway()->read($postId, $area);
        $depth    = isset($params['depth']) ? (int) $params['depth'] : TreeSummarizer::DEFAULT_DEPTH;
        $expand   = (string) ($params['expand'] ?? '');
        $summary  = $this->summarizer()->summarize($elements, $depth, $expand);

        return [
            'success'   => true,
            'message'   => $summary['totals']['elements'] === 0
                ? 'This page area is empty.'
                : 'Here is the page structure summary.',
            'post_id'   => $postId,
            'area'      => $area,
            'summary'   => $summary['nodes'],
            'totals'    => $summary['totals'],
            'truncated' => $summary['truncated'],
        ];
    }

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

        $name = sanitize_key((string) ($params['name'] ?? ''));
        if ($name === '') {
            return $this->failure('I need an element type to insert. What should I add?', 'missing_name');
        }

        $settings = is_array($params['settings'] ?? null) ? $params['settings'] : [];
        if ($name === 'code' && isset($settings['executeCode'])) {
            return BricksCodeGuard::rejectionResponse(['executeCode' => $settings['executeCode']])
                ?? $this->failure(
                    'I cannot save Bricks code execution settings (executeCode). Remove it and try again.',
                    'forbidden_code'
                );
        }

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

        /** @var ElementTree $tree */
        $tree     = $loaded['tree'];
        $element  = [
            'id'       => $this->ids()->generate($tree->ids()),
            'name'     => $name,
            'parent'   => ElementTree::ROOT_PARENT,
            'children' => [],
            'settings' => $settings,
        ];
        $position = $this->position($params);
        $error    = $tree->insert($element, $position, $this->insertReference($params, $position));
        if ($error !== null) {
            return $this->treeFailure($error);
        }

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            $tree,
            $dryRun,
            'Insert preview is ready.',
            'I inserted the element. Refresh the front to see it.',
            [
                'element_id' => $element['id'],
                'name'       => $name,
            ]
        );
    }

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

        $elementId = (string) ($params['element_id'] ?? '');
        if ($elementId === '') {
            return $this->failure('I need the element to move. Which one?', 'missing_element');
        }

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

        /** @var ElementTree $tree */
        $tree     = $loaded['tree'];
        $position = $this->position($params);
        $error    = $tree->move($elementId, $position, $this->insertReference($params, $position));
        if ($error !== null) {
            return $this->treeFailure($error);
        }

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            $tree,
            $dryRun,
            'Move preview is ready.',
            'I moved the element. Refresh the front to see it.',
            ['element_id' => $elementId]
        );
    }

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

        $elementId = (string) ($params['element_id'] ?? '');
        if ($elementId === '') {
            return $this->failure('I need the element to duplicate. Which one?', 'missing_element');
        }

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

        /** @var ElementTree $tree */
        $tree   = $loaded['tree'];
        $result = $tree->duplicate($elementId, $this->ids());
        if (($result['error'] ?? null) !== null) {
            return $this->treeFailure((string) $result['error']);
        }

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            $tree,
            $dryRun,
            'Duplicate preview is ready.',
            'I duplicated the element with new IDs. Refresh the front to see it.',
            [
                'element_id'     => $elementId,
                'new_element_id' => $result['new_id'] ?? '',
                'id_map'         => $result['id_map'] ?? [],
            ]
        );
    }

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

        $elementId = (string) ($params['element_id'] ?? '');
        if ($elementId === '') {
            return $this->failure('I need the element to remove. Which one?', 'missing_element');
        }

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

        /** @var ElementTree $tree */
        $tree         = $loaded['tree'];
        $removedCount = 1 + count($tree->descendantIds($elementId));
        $error        = $tree->delete($elementId);
        if ($error !== null) {
            return $this->treeFailure($error);
        }

        return $this->persist(
            $loaded['post_id'],
            $loaded['area'],
            $tree,
            $dryRun,
            'Remove preview is ready.',
            'I removed the element. You can restore the previous version from the page revisions.',
            [
                'element_id'    => $elementId,
                'removed_count' => $removedCount,
            ]
        );
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function loadTree(array $params): array
    {
        $postId = $this->postId($params);
        $area   = $this->area($params);
        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 array<string, mixed> $extra
     * @return array<string, mixed>
     */
    private function persist(
        int $postId,
        string $area,
        ElementTree $tree,
        bool $dryRun,
        string $previewMessage,
        string $doneMessage,
        array $extra
    ): array {
        $written = $this->gateway()->write($postId, $area, $tree->toList(), $dryRun);
        if (($written['success'] ?? false) !== true) {
            return $written;
        }

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

        return array_merge(
            $written,
            $extra,
            [
                'message' => $dryRun ? $previewMessage : $doneMessage,
                'summary' => $summary['nodes'],
                'totals'  => $summary['totals'],
            ]
        );
    }

    /**
     * @param array<string, mixed> $params
     */
    private function insertReference(array $params, string $position): string
    {
        if ($position === ElementTree::POSITION_CHILD) {
            return (string) ($params['parent_id'] ?? $params['reference_id'] ?? '');
        }

        return (string) ($params['reference_id'] ?? $params['parent_id'] ?? '');
    }

    /**
     * @param array<string, mixed> $params
     */
    private function position(array $params): string
    {
        $position = strtolower((string) ($params['position'] ?? ElementTree::POSITION_CHILD));

        return in_array($position, [
            ElementTree::POSITION_CHILD,
            ElementTree::POSITION_BEFORE,
            ElementTree::POSITION_AFTER,
        ], true) ? $position : ElementTree::POSITION_CHILD;
    }

    /**
     * @param array<string, mixed> $params
     */
    private function postId(array $params): int
    {
        return absint($params['post_id'] ?? $params['postId'] ?? 0);
    }

    /**
     * @param array<string, mixed> $params
     */
    private function area(array $params): string
    {
        return $this->gateway()->resolveArea((string) ($params['area'] ?? BricksMetaKeys::AREA_CONTENT));
    }

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

        if (!BricksEnv::userCanReadDesignSystem()) {
            return $this->failure(
                'I need administrator access to change Bricks page structure.',
                '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 treeFailure(string $error): array
    {
        $messages = [
            'element_not_found'   => 'I could not find that element on this page.',
            'reference_not_found' => 'I could not find the placement target on this page.',
            'cycle'               => 'That move would nest the element inside itself. I left the tree as it is.',
            'invalid_id'          => 'I could not create a unique element ID. Try again.',
            'missing_parent'      => 'The page tree is inconsistent (a parent is missing). I will not write that.',
            'missing_child'       => 'The page tree is inconsistent (a child is missing). I will not write that.',
            'parent_mismatch'     => 'The page tree is inconsistent (parent and children do not match). I will not write that.',
            'orphan'              => 'The page tree is inconsistent (an element would be left orphaned). I will not write that.',
            'duplicate_id'        => 'The page tree is inconsistent (duplicate IDs). I will not write that.',
            'missing_id'          => 'The page tree is inconsistent (an element has no ID). I will not write that.',
        ];

        return $this->failure(
            $messages[$error] ?? 'I could not update the page tree without breaking it.',
            $error
        );
    }

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

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