<?php
/**
 * Global class write handlers with dry-run previews.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Handlers;

use LolaCoreForBricks\BricksEnv;
use LolaCoreForBricks\Gateways\ClassGateway;
use LolaCoreForBricks\Index\IndexBuilder;
use LolaCoreForBricks\Index\IndexReader;
use LolaCoreForBricks\Index\IndexRepository;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\CoveragePayload;
use LolaCoreForBricks\Support\CssRegenerator;
use LolaCoreForBricks\Support\ImpactPayload;
use LolaCoreForBricks\Support\SystemVocabulary;

/**
 * Executes B4 global class write abilities.
 */
class ClassHandler
{
    public function __construct(
        private readonly ?ClassGateway $gateway = null,
        private readonly ?SystemVocabulary $vocabulary = null,
        private readonly ?CssRegenerator $cssRegenerator = null,
        private readonly ?IndexReader $indexReader = null
    ) {
    }

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

        $name     = trim((string) ($params['name'] ?? ''));
        $settings = is_array($params['settings'] ?? null) ? $params['settings'] : [];
        $notes    = ($this->vocabulary ?? new SystemVocabulary())->notesBeforeCreate($name, $settings);
        $payload  = [
            'vocabulary_notes' => $notes,
            'coverage'         => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Create preview is ready.', $payload);
        }

        $result = ($this->gateway ?? new ClassGateway())->createGlobalClass($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        (new IndexBuilder())->indexGlobalScopes(new IndexRepository());

        return $this->wrapExecuted($result, $payload);
    }

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

        $resolved = ($this->gateway ?? new ClassGateway())->resolveClassRef(
            (string) ($params['class'] ?? $params['class_id'] ?? '')
        );
        if ($resolved === null) {
            return $this->failure('I could not find that global class.', 'class_not_found');
        }

        $payload = [
            'impact'   => ImpactPayload::forClass($resolved['id'], $this->indexReader),
            'coverage' => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Update preview is ready.', $payload);
        }

        $result = ($this->gateway ?? new ClassGateway())->updateGlobalClassSettings($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        $this->afterClassWrite((string) $result['class_id']);

        return $this->wrapExecuted($result, $payload);
    }

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

        $resolved = ($this->gateway ?? new ClassGateway())->resolveClassRef(
            (string) ($params['class'] ?? $params['class_id'] ?? '')
        );
        if ($resolved === null) {
            return $this->failure('I could not find that global class.', 'class_not_found');
        }

        $payload = [
            'impact'   => ImpactPayload::forClass($resolved['id'], $this->indexReader),
            'coverage' => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Rename preview is ready.', $payload);
        }

        $result = ($this->gateway ?? new ClassGateway())->renameGlobalClass($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        $this->afterClassWrite((string) $result['class_id'], $result['affected_post_ids'] ?? []);

        return $this->wrapExecuted($result, $payload);
    }

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

        $gateway = $this->gateway ?? new ClassGateway();
        $source  = $gateway->resolveClassRef((string) ($params['source_class'] ?? $params['source'] ?? ''));
        $target  = $gateway->resolveClassRef((string) ($params['target_class'] ?? $params['target'] ?? ''));

        if ($source === null || $target === null) {
            return $this->failure(
                'I could not find both classes to merge. Check the source and target names or ids.',
                'class_not_found'
            );
        }

        $payload = [
            'impact'   => ImpactPayload::forClass($source['id'], $this->indexReader),
            'coverage' => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Merge preview is ready.', $payload);
        }

        $result = $gateway->mergeGlobalClasses($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        $affected = is_array($result['affected_post_ids'] ?? null) ? $result['affected_post_ids'] : [];
        (new IndexRepository())->reindexPosts($affected);
        (new IndexBuilder())->indexGlobalScopes(new IndexRepository());
        ($this->cssRegenerator ?? new CssRegenerator())->afterClassChange($affected);

        return $this->wrapExecuted($result, $payload);
    }

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

        $refs = $params['classes'] ?? $params['class_ids'] ?? $params['class'] ?? [];
        if (!is_array($refs)) {
            $refs = [$refs];
        }

        $classIds = [];
        $postIds  = [];
        $gateway  = $this->gateway ?? new ClassGateway();
        $reader   = $this->indexReader ?? new IndexReader();
        foreach ($refs as $ref) {
            $resolved = $gateway->resolveClassRef((string) $ref);
            if ($resolved === null) {
                continue;
            }

            $classIds[] = $resolved['id'];
            foreach ($reader->usageOf(BricksMetaKeys::REF_CLASS, $resolved['id']) as $edge) {
                $postId = (int) ($edge['post_id'] ?? 0);
                if ($postId > 0) {
                    $postIds[$postId] = $postId;
                }
            }
        }

        $payload = [
            'impact'   => ImpactPayload::forClasses($classIds, $reader),
            'coverage' => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Trash preview is ready.', $payload);
        }

        $result = $gateway->trashGlobalClasses($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        (new IndexBuilder())->indexGlobalScopes(new IndexRepository());
        ($this->cssRegenerator ?? new CssRegenerator())->afterClassChange(array_values($postIds));

        return $this->wrapExecuted($result, $payload);
    }

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

        $payload = [
            'coverage' => CoveragePayload::forResponse(),
        ];

        if ($dryRun) {
            return $this->preview('Restore preview is ready.', $payload);
        }

        $result = ($this->gateway ?? new ClassGateway())->restoreGlobalClass($params);
        if (($result['success'] ?? false) !== true) {
            return $result;
        }

        (new IndexBuilder())->indexGlobalScopes(new IndexRepository());

        return $this->wrapExecuted($result, $payload);
    }

    /**
     * @param int[] $affectedPostIds
     */
    private function afterClassWrite(string $classId, array $affectedPostIds = []): void
    {
        if ($affectedPostIds === []) {
            $affectedPostIds = array_map(
                static fn (array $edge): int => (int) ($edge['post_id'] ?? 0),
                ($this->indexReader ?? new IndexReader())->usageOf(BricksMetaKeys::REF_CLASS, $classId)
            );
        }

        $affectedPostIds = array_values(array_filter(array_map('intval', $affectedPostIds)));
        (new IndexRepository())->reindexPosts($affectedPostIds);
        (new IndexBuilder())->indexGlobalScopes(new IndexRepository());
        ($this->cssRegenerator ?? new CssRegenerator())->afterClassChange($affectedPostIds);
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function preview(string $message, array $payload): array
    {
        return array_merge(
            [
                'success' => true,
                'message' => $message,
                'dry_run' => true,
            ],
            $payload
        );
    }

    /**
     * @param array<string, mixed> $result
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function wrapExecuted(array $result, array $payload): array
    {
        unset($result['affected_post_ids']);

        return array_merge($result, $payload);
    }

    /**
     * @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 global classes.',
                '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,
        ];
    }
}
