<?php

namespace QUI\InfoPopup\Api;

use InvalidArgumentException;
use QUI;
use QUI\InfoPopup\Handler;
use Throwable;

use function array_filter;
use function array_key_exists;
use function array_map;
use function array_slice;
use function array_values;
use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use function max;
use function min;
use function stripos;
use function strtotime;

/**
 * Shared info popup operations for machine-facing APIs such as MCP.
 */
class PopupService
{
    private const WRITABLE_FIELDS = [
        'active',
        'priority',
        'title',
        'content',
        'validFrom',
        'validTo',
        'visibility',
        'visibilityGroups',
        'displayMode',
        'verticalAlign',
        'centerContent',
        'mediaImage',
        'mediaColor',
        'mediaAutoHeight',
        'maxWidth',
        'maxHeight'
    ];

    /**
     * @param array<string, mixed> $attributes
     * @return array<string, mixed>
     */
    public function create(string $project, array $attributes = []): array
    {
        $Project = QUI::getProject($project);
        $attributes = self::normalizeAttributes($attributes);
        $Connection = QUI::getDataBaseConnection();
        $Connection->beginTransaction();

        try {
            $id = Handler::create($Project);
            $data = [
                'active' => false,
                'priority' => 0,
                'title' => [],
                'content' => [],
                'validFrom' => null,
                'validTo' => null,
                'visibility' => 'always',
                'visibilityGroups' => [],
                'displayMode' => '',
                'verticalAlign' => 'top',
                'centerContent' => false,
                'mediaImage' => null,
                'mediaColor' => null,
                'mediaAutoHeight' => false,
                'maxWidth' => null,
                'maxHeight' => null
            ];

            foreach (self::WRITABLE_FIELDS as $field) {
                if (array_key_exists($field, $attributes)) {
                    $data[$field] = $attributes[$field];
                }
            }

            self::validateValidityWindow($data);
            Handler::save($id, $data);
            $popup = $this->get($project, $id);
            $Connection->commit();

            return $popup;
        } catch (Throwable $Exception) {
            if ($Connection->isTransactionActive()) {
                $Connection->rollBack();
            }

            throw $Exception;
        }
    }

    /**
     * @return array<string, mixed>
     */
    public function get(string $project, int $id): array
    {
        $entry = Handler::getEntry($id);
        $this->assertProject($entry, $project);

        return self::serialize($entry);
    }

    /**
     * @param array<string, mixed> $attributes
     * @return array<string, mixed>
     */
    public function update(string $project, int $id, array $attributes): array
    {
        $Connection = QUI::getDataBaseConnection();
        $Connection->beginTransaction();

        try {
            $entry = Handler::getEntryForUpdate($id, $project);
            $attributes = self::normalizeAttributes($attributes);

            foreach (self::WRITABLE_FIELDS as $field) {
                if (array_key_exists($field, $attributes)) {
                    $entry[$field] = $attributes[$field];
                }
            }

            self::validateValidityWindow($entry);
            Handler::save($id, $entry);
            $popup = $this->get($project, $id);
            $Connection->commit();

            return $popup;
        } catch (Throwable $Exception) {
            if ($Connection->isTransactionActive()) {
                $Connection->rollBack();
            }

            throw $Exception;
        }
    }

    /**
     * @return array{deleted: true, id: int}
     */
    public function delete(string $project, int $id): array
    {
        $Connection = QUI::getDataBaseConnection();
        $Connection->beginTransaction();

        try {
            Handler::getEntryForUpdate($id, $project);
            Handler::delete($id);
            $Connection->commit();
        } catch (Throwable $Exception) {
            if ($Connection->isTransactionActive()) {
                $Connection->rollBack();
            }

            throw $Exception;
        }

        return [
            'deleted' => true,
            'id' => $id
        ];
    }

    /**
     * @return array{project: string, total: int, popups: array<int, array<string, mixed>>}
     */
    public function getList(
        string $project,
        ?string $search = null,
        ?bool $active = null,
        ?int $limit = null,
        ?int $offset = null
    ): array {
        $entries = array_map(
            static fn(array $entry): array => self::serialize($entry),
            Handler::getList(QUI::getProject($project))
        );

        $entries = array_values(array_filter(
            $entries,
            static function (array $entry) use ($search, $active): bool {
                if ($active !== null && $entry['active'] !== $active) {
                    return false;
                }

                if ($search === null || $search === '') {
                    return true;
                }

                $searchable = json_encode([$entry['title'], $entry['content']], JSON_UNESCAPED_UNICODE);

                return is_string($searchable) && stripos($searchable, $search) !== false;
            }
        ));

        $total = count($entries);
        $limit = min(100, max(1, $limit ?? 50));
        $offset = max(0, $offset ?? 0);

        return [
            'project' => $project,
            'total' => $total,
            'popups' => array_slice($entries, $offset, $limit)
        ];
    }

    /**
     * @param array<string, mixed> $entry
     * @return array<string, mixed>
     */
    private static function serialize(array $entry): array
    {
        return [
            'id' => (int)$entry['id'],
            'project' => (string)$entry['project'],
            'active' => !empty($entry['active']),
            'priority' => (int)($entry['priority'] ?? 0),
            'title' => self::decodeMultilingualValue($entry['title'] ?? null),
            'content' => self::decodeMultilingualValue($entry['content'] ?? null),
            'validFrom' => $entry['validFrom'] ?: null,
            'validTo' => $entry['validTo'] ?: null,
            'visibility' => (string)($entry['visibility'] ?? 'always'),
            'visibilityGroupIds' => Handler::parseGroupIds($entry['visibilityGroups'] ?? null),
            'displayMode' => (string)($entry['displayMode'] ?? ''),
            'verticalAlign' => (string)($entry['verticalAlign'] ?? 'top'),
            'centerContent' => !empty($entry['centerContent']),
            'mediaImage' => $entry['mediaImage'] ?: null,
            'mediaColor' => $entry['mediaColor'] ?: null,
            'mediaAutoHeight' => !empty($entry['mediaAutoHeight']),
            'maxWidth' => empty($entry['maxWidth']) ? null : (int)$entry['maxWidth'],
            'maxHeight' => empty($entry['maxHeight']) ? null : (int)$entry['maxHeight'],
            'editDate' => (int)($entry['editDate'] ?? 0)
        ];
    }

    /**
     * @return array<string, string>|string
     */
    private static function decodeMultilingualValue(mixed $value): array|string
    {
        if (!is_string($value) || $value === '') {
            return [];
        }

        $decoded = json_decode($value, true);

        if (!is_array($decoded)) {
            return $value;
        }

        $result = [];

        foreach ($decoded as $language => $text) {
            if (is_string($language) && is_string($text)) {
                $result[$language] = $text;
            }
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $attributes
     * @return array<string, mixed>
     */
    private static function normalizeAttributes(array $attributes): array
    {
        if (array_key_exists('visibilityGroupIds', $attributes)) {
            $attributes['visibilityGroups'] = $attributes['visibilityGroupIds'];
            unset($attributes['visibilityGroupIds']);
        }

        return $attributes;
    }

    /**
     * @param array<string, mixed> $data
     */
    private static function validateValidityWindow(array $data): void
    {
        $validFrom = empty($data['validFrom']) ? null : strtotime((string)$data['validFrom']);
        $validTo = empty($data['validTo']) ? null : strtotime((string)$data['validTo']);

        if (!empty($data['validFrom']) && $validFrom === false) {
            throw new InvalidArgumentException('validFrom must be a valid date and time.');
        }

        if (!empty($data['validTo']) && $validTo === false) {
            throw new InvalidArgumentException('validTo must be a valid date and time.');
        }

        if ($validFrom !== null && $validTo !== null && $validTo < $validFrom) {
            throw new InvalidArgumentException('validTo must not be earlier than validFrom.');
        }
    }

    /**
     * @param array<string, mixed> $entry
     * @throws QUI\Exception
     */
    private function assertProject(array $entry, string $project): void
    {
        if ((string)($entry['project'] ?? '') !== $project) {
            throw new QUI\Exception('Info popup entry not found', 404);
        }
    }
}
