<?php
/**
 * Global class writes for Bricks options.
 *
 * @package LolaCoreForBricks
 */

declare(strict_types=1);

namespace LolaCoreForBricks\Gateways;

use LolaCoreForBricks\Index\ClassCascade;
use LolaCoreForBricks\Index\IndexReader;
use LolaCoreForBricks\Index\IndexRepository;
use LolaCoreForBricks\Support\BricksMetaKeys;
use LolaCoreForBricks\Support\BricksCodeGuard;
use LolaCoreForBricks\Support\CssTextGuard;
use LolaCoreForBricks\Support\SettingsKeyGrammar;

/**
 * Writes to bricks_global_classes, trash, and related meta.
 */
class ClassWriteGateway
{
    public function __construct(
        private readonly ClassGateway $readGateway = new ClassGateway()
    ) {
    }

    public function createGlobalClass(array $params): array
    {
        $name     = trim((string) ($params['name'] ?? ''));
        $category = (string) ($params['category'] ?? '');
        $settings = is_array($params['settings'] ?? null) ? $params['settings'] : [];

        if ($name === '') {
            return $this->failure('I need a class name to create one.', 'missing_name');
        }

        if ($rejection = BricksCodeGuard::validateSettings($settings)) {
            return $rejection;
        }

        if ($rejection = CssTextGuard::validateSettings($settings)) {
            return $rejection;
        }

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        if ($this->findClassByName($classes, $name) !== null) {
            return $this->failure(
                'A global class with that name already exists. Pick a different name or merge with the existing class.',
                'duplicate_name'
            );
        }

        $class = [
            'id'       => $this->generateClassId($classes),
            'name'     => $name,
            'category' => $category,
            'settings' => $this->normalizeIncomingSettings($settings),
            'modified' => time(),
            'user'     => $this->currentUserId(),
        ];

        $classes[] = $class;
        $this->saveActiveClasses($classes);

        return [
            'success'  => true,
            'message'  => 'Global class created.',
            'class'    => $this->summarizeClass($class),
            'class_id' => $class['id'],
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function updateGlobalClassSettings(array $params): array
    {
        $resolved = $this->resolveWritableClass($params);
        if (isset($resolved['success']) && $resolved['success'] === false) {
            return $resolved;
        }

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

        if ($rejection = BricksCodeGuard::validateSettings($settings)) {
            return $rejection;
        }

        if ($rejection = CssTextGuard::validateSettings($settings)) {
            return $rejection;
        }

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        $index   = (int) $resolved['index'];
        $merged  = array_merge(
            is_array($classes[$index]['settings'] ?? null) ? $classes[$index]['settings'] : [],
            $this->normalizeIncomingSettings($settings)
        );

        $classes[$index]['settings'] = $merged;
        $classes[$index]['modified'] = time();
        $classes[$index]['user']     = $this->currentUserId();
        $this->saveActiveClasses($classes);

        return [
            'success'  => true,
            'message'  => 'Global class settings updated.',
            'class'    => $this->summarizeClass($classes[$index]),
            'class_id' => (string) $classes[$index]['id'],
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function renameGlobalClass(array $params): array
    {
        $resolved = $this->resolveWritableClass($params);
        if (isset($resolved['success']) && $resolved['success'] === false) {
            return $resolved;
        }

        $newName = trim((string) ($params['new_name'] ?? $params['name'] ?? ''));
        if ($newName === '') {
            return $this->failure('I need the new class name to rename it.', 'missing_new_name');
        }

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        $index   = (int) $resolved['index'];
        $classId = (string) $classes[$index]['id'];

        if (strcasecmp((string) ($classes[$index]['name'] ?? ''), $newName) === 0) {
            return $this->failure('That class already uses this name.', 'unchanged_name');
        }

        if ($this->findClassByName($classes, $newName, $classId) !== null) {
            return $this->failure(
                'Another global class already uses that name. Pick a different name.',
                'duplicate_name'
            );
        }

        $classes[$index]['name']     = $newName;
        $classes[$index]['modified'] = time();
        $classes[$index]['user']     = $this->currentUserId();
        $this->saveActiveClasses($classes);

        return [
            'success'         => true,
            'message'         => 'Global class renamed.',
            'class'           => $this->summarizeClass($classes[$index]),
            'class_id'        => $classId,
            'affected_post_ids' => $this->affectedPostIds($classId),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function mergeGlobalClasses(array $params): array
    {
        $source = $this->readGateway->resolveClassRef((string) ($params['source_class'] ?? $params['source'] ?? ''));
        $target = $this->readGateway->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');
        }

        if ($source['id'] === $target['id']) {
            return $this->failure('Source and target classes must be different.', 'same_class');
        }

        $affectedPostIds = (new ClassCascade())->replaceClassId($source['id'], $target['id']);

        $repository = new IndexRepository();
        $repository->replaceRefId(BricksMetaKeys::REF_CLASS, $source['id'], $target['id']);

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        $sourceClass = $this->findRawClassById($classes, $source['id']);
        if ($sourceClass === null) {
            return $this->failure('The source class disappeared before the merge finished.', 'source_missing');
        }

        $classes = array_values(array_filter(
            $classes,
            static fn (array $class): bool => (string) ($class['id'] ?? '') !== $source['id']
        ));

        $trash   = $this->loadRawClasses('bricks_global_classes_trash');
        $trash[] = $sourceClass;
        update_option('bricks_global_classes_trash', array_values($trash));
        $this->saveActiveClasses($classes);

        return [
            'success'           => true,
            'message'           => 'Global classes merged. The source class is in the Bricks trash.',
            'source_class'      => $source,
            'target_class'      => $target,
            'affected_post_ids' => $affectedPostIds,
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function trashGlobalClasses(array $params): array
    {
        $refs = $params['classes'] ?? $params['class_ids'] ?? $params['class'] ?? [];
        if (!is_array($refs)) {
            $refs = [$refs];
        }

        $refs = array_values(array_filter(array_map('strval', $refs)));
        if ($refs === []) {
            return $this->failure('Tell me which global classes to move to the trash.', 'missing_classes');
        }

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        $trash   = $this->loadRawClasses('bricks_global_classes_trash');
        $trashed = [];

        foreach ($refs as $ref) {
            $resolved = $this->readGateway->resolveClassRef($ref);
            if ($resolved === null) {
                continue;
            }

            $raw = $this->findRawClassById($classes, $resolved['id']);
            if ($raw === null) {
                continue;
            }

            $classes = array_values(array_filter(
                $classes,
                static fn (array $class): bool => (string) ($class['id'] ?? '') !== $resolved['id']
            ));
            $trash[]   = $raw;
            $trashed[] = $this->summarizeClass($raw);
        }

        if ($trashed === []) {
            return $this->failure('I could not find any of those classes in the active library.', 'class_not_found');
        }

        update_option('bricks_global_classes_trash', array_values($trash));
        $this->saveActiveClasses($classes);

        return [
            'success'  => true,
            'message'  => 'Selected global classes moved to the Bricks trash.',
            'trashed'  => $trashed,
            'count'    => count($trashed),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    public function restoreGlobalClass(array $params): array
    {
        $ref = (string) ($params['class'] ?? $params['class_id'] ?? '');
        if ($ref === '') {
            return $this->failure('Tell me which trashed class to restore.', 'missing_class');
        }

        $trash = $this->loadRawClasses('bricks_global_classes_trash');
        $match = null;
        $index = null;

        foreach ($trash as $key => $class) {
            if ((string) ($class['id'] ?? '') === $ref || strcasecmp((string) ($class['name'] ?? ''), $ref) === 0) {
                $match = $class;
                $index = $key;
                break;
            }
        }

        if (!is_array($match) || $index === null) {
            return $this->failure('I could not find that class in the Bricks trash.', 'class_not_in_trash');
        }

        unset($trash[$index]);
        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);

        $name = (string) ($match['name'] ?? $match['id']);
        if ($this->findClassByName($classes, $name) !== null) {
            $name .= '-restored';
            $match['name'] = $name;
        }

        $match['modified'] = time();
        $match['user']     = $this->currentUserId();
        $classes[]         = $match;

        update_option('bricks_global_classes_trash', array_values($trash));
        $this->saveActiveClasses($classes);

        return [
            'success'  => true,
            'message'  => 'Global class restored from the Bricks trash.',
            'class'    => $this->summarizeClass($match),
            'class_id' => (string) ($match['id'] ?? ''),
        ];
    }

    /**
     * @param array<string, mixed> $params
     * @return array<string, mixed>
     */
    private function resolveWritableClass(array $params): array
    {
        $ref = (string) ($params['class'] ?? $params['class_id'] ?? '');
        if ($ref === '') {
            return $this->failure('Tell me which global class to update.', 'missing_class');
        }

        $classes = $this->loadRawClasses(BricksMetaKeys::OPTION_GLOBAL_CLASSES);
        foreach ($classes as $index => $class) {
            if ((string) ($class['id'] ?? '') === $ref || strcasecmp((string) ($class['name'] ?? ''), $ref) === 0) {
                return [
                    'index' => $index,
                    'class' => $class,
                ];
            }
        }

        return $this->failure('I could not find that global class.', 'class_not_found');
    }

    /**
     * @param array<int, array<string, mixed>> $classes
     */
    private function saveActiveClasses(array $classes): void
    {
        update_option(BricksMetaKeys::OPTION_GLOBAL_CLASSES, array_values($classes));
        $this->touchGlobalClassesMeta();
    }

    private function touchGlobalClassesMeta(): void
    {
        update_option(BricksMetaKeys::OPTION_CLASS_TIMESTAMP, time());
        update_option('bricks_global_classes_user', $this->currentUserId());
    }

    /**
     * @return int[]
     */
    private function affectedPostIds(string $classId): array
    {
        $postIds = [];
        foreach ((new IndexReader())->usageOf(BricksMetaKeys::REF_CLASS, $classId) as $edge) {
            $postId = (int) ($edge['post_id'] ?? 0);
            if ($postId > 0) {
                $postIds[$postId] = $postId;
            }
        }

        return array_values($postIds);
    }

    /**
     * @param array<int, array<string, mixed>> $classes
     */
    private function generateClassId(array $classes): string
    {
        $existing = [];
        foreach ($classes as $class) {
            if (!empty($class['id'])) {
                $existing[(string) $class['id']] = true;
            }
        }

        do {
            $id = class_exists(\Bricks\Helpers::class)
                ? (string) \Bricks\Helpers::generate_random_id(false)
                : substr(md5(uniqid('', true)), 0, 6);
        } while (isset($existing[$id]));

        return $id;
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function normalizeIncomingSettings(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<int, array<string, mixed>> $classes
     * @return array<string, mixed>|null
     */
    private function findClassByName(array $classes, string $name, ?string $exceptId = null): ?array
    {
        foreach ($classes as $class) {
            if ($exceptId !== null && (string) ($class['id'] ?? '') === $exceptId) {
                continue;
            }

            if (strcasecmp((string) ($class['name'] ?? ''), $name) === 0) {
                return $class;
            }
        }

        return null;
    }

    /**
     * @param array<int, array<string, mixed>> $classes
     * @return array<string, mixed>|null
     */
    private function findRawClassById(array $classes, string $classId): ?array
    {
        foreach ($classes as $class) {
            if ((string) ($class['id'] ?? '') === $classId) {
                return $class;
            }
        }

        return null;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function loadRawClasses(string $option): array
    {
        $raw = get_option($option, []);
        if (!is_array($raw)) {
            return [];
        }

        $classes = [];
        foreach ($raw as $class) {
            if (is_array($class) && !empty($class['id'])) {
                $classes[] = $class;
            }
        }

        return $classes;
    }

    /**
     * @param array<string, mixed> $class
     * @return array<string, mixed>
     */
    private function summarizeClass(array $class): array
    {
        $settings = is_array($class['settings'] ?? null) ? $class['settings'] : [];

        return [
            'id'             => (string) ($class['id'] ?? ''),
            'name'           => (string) ($class['name'] ?? ''),
            'category'       => (string) ($class['category'] ?? ''),
            'settings_count' => count($settings),
            'modified'       => isset($class['modified']) ? (int) $class['modified'] : 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 currentUserId(): int
    {
        return function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
    }

}
