<?php

namespace MintyPHP\Module\Security\Service;

use MintyPHP\Module\Security\Repository\SecurityCheckTemplateRepository;

/**
 * Business logic for security check templates (the per-product checklist catalog).
 *
 * A template owns the dynamic technical checklist for one software product. The
 * checklist schema shape is:
 *   { "version": int, "items": [ {type:"section", label}, {type:"check", key, label, hint?, markdown?} ] }
 * Item keys are auto-slugified from labels and kept unique within a template.
 *
 * @api Consumed by Security template pages and the create wizard.
 */
class SecurityCheckTemplateService
{
    public const MAX_ITEMS = 100;

    private const ALLOWED_ITEM_TYPES = ['section', 'check'];

    public function __construct(
        private readonly SecurityCheckTemplateRepository $repository
    ) {
    }

    /**
     * @api Called from the create wizard product picker
     * @return list<array<string, mixed>>
     */
    public function listActive(int $tenantId): array
    {
        return $this->repository->listActive($tenantId);
    }

    /**
     * @api Called from templates-data endpoint
     * @param array<string, mixed> $filters
     * @return array{total: int, rows: list<array<string, mixed>>}
     */
    public function listPaged(int $tenantId, array $filters): array
    {
        return $this->repository->listPaged($tenantId, $filters);
    }

    public function findById(int $tenantId, int $id): ?array
    {
        return $this->repository->findById($tenantId, $id);
    }

    public function findByCode(int $tenantId, string $code): ?array
    {
        return $this->repository->findByCode($tenantId, $code);
    }

    /**
     * Decode a template's stored tech schema into {version, items}.
     *
     * @param array<string, mixed> $template
     * @return array{version: int, items: list<array<string, mixed>>}
     */
    public function decodeSchema(array $template): array
    {
        $decoded = json_decode((string) ($template['tech_schema_json'] ?? ''), true);
        if (!is_array($decoded)) {
            return ['version' => (int) ($template['version'] ?? 1), 'items' => []];
        }

        $items = is_array($decoded['items'] ?? null) ? array_values($decoded['items']) : [];

        return ['version' => (int) ($decoded['version'] ?? ($template['version'] ?? 1)), 'items' => $items];
    }

    /**
     * @return array{ok: bool, id?: int, errors: array<string, string>}
     */
    public function create(int $tenantId, string $code, string $name, int $actorUserId): array
    {
        $code = trim($code);
        $name = trim($name);

        $errors = [];
        if ($tenantId <= 0) {
            $errors['tenant'] = t('No tenant context');
        }
        if ($code === '') {
            $errors['product_code'] = t('Product code cannot be empty');
        } elseif (!preg_match('/^[A-Za-z0-9._-]+$/', $code)) {
            $errors['product_code'] = t('Product code is invalid');
        } elseif ($this->repository->codeExists($tenantId, $code)) {
            $errors['product_code'] = t('A template with this product code already exists');
        }
        if ($name === '') {
            $errors['product_name'] = t('Product name cannot be empty');
        }
        if ($errors !== []) {
            return ['ok' => false, 'errors' => $errors];
        }

        $id = $this->repository->insert($tenantId, [
            'product_code' => $code,
            'product_name' => $name,
            'tech_schema_json' => json_encode(['version' => 1, 'items' => []]),
            'active' => 1,
            'version' => 1,
            'created_by' => $actorUserId,
        ]);

        if ($id === null) {
            return ['ok' => false, 'errors' => ['general' => t('Template could not be created')]];
        }

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

    /**
     * @return array{ok: bool, errors: array<string, string>}
     */
    public function updateMeta(int $tenantId, int $id, string $name, bool $active, int $actorUserId): array
    {
        $name = trim($name);
        if ($name === '') {
            return ['ok' => false, 'errors' => ['product_name' => t('Product name cannot be empty')]];
        }
        if ($this->repository->findById($tenantId, $id) === null) {
            return ['ok' => false, 'errors' => ['general' => t('Template not found')]];
        }

        $ok = $this->repository->updateMeta($tenantId, $id, $name, $active, $actorUserId);

        return $ok ? ['ok' => true, 'errors' => []] : ['ok' => false, 'errors' => ['general' => t('Template could not be updated')]];
    }

    /**
     * Validate + normalize the technical checklist and persist it (version bump).
     *
     * @param array<int, mixed> $items
     * @return array{ok: bool, errors: array<string, string>, version?: int}
     */
    public function saveTechSchema(int $tenantId, int $id, array $items, int $actorUserId): array
    {
        $template = $this->repository->findById($tenantId, $id);
        if ($template === null) {
            return ['ok' => false, 'errors' => ['general' => t('Template not found')]];
        }

        if (count($items) > self::MAX_ITEMS) {
            return ['ok' => false, 'errors' => ['schema' => t('Maximum %d items allowed', self::MAX_ITEMS)]];
        }

        $normalized = [];
        $usedKeys = [];
        foreach ($items as $index => $item) {
            if (!is_array($item)) {
                continue;
            }
            $type = (string) ($item['type'] ?? 'check');
            if (!in_array($type, self::ALLOWED_ITEM_TYPES, true)) {
                return ['ok' => false, 'errors' => ['schema' => t('Item %d has an invalid type', $index + 1)]];
            }
            $label = trim((string) ($item['label'] ?? ''));
            if ($label === '') {
                return ['ok' => false, 'errors' => ['schema' => t('Item %d is missing a label', $index + 1)]];
            }

            if ($type === 'section') {
                $normalized[] = ['type' => 'section', 'label' => $label];
                continue;
            }

            $key = trim((string) ($item['key'] ?? ''));
            if ($key === '') {
                $key = self::slugify($label);
            }
            $key = self::slugify($key);
            if ($key === '') {
                return ['ok' => false, 'errors' => ['schema' => t('Item %d has an invalid key', $index + 1)]];
            }
            if (isset($usedKeys[$key])) {
                $suffix = 2;
                while (isset($usedKeys[$key . '_' . $suffix])) {
                    $suffix++;
                }
                $key .= '_' . $suffix;
            }
            $usedKeys[$key] = true;

            $entry = ['type' => 'check', 'key' => $key, 'label' => $label];
            $hint = trim((string) ($item['hint'] ?? ''));
            if ($hint !== '') {
                $entry['hint'] = $hint;
            }
            $markdown = trim((string) ($item['markdown'] ?? ''));
            if ($markdown !== '') {
                $entry['markdown'] = $markdown;
            }
            $normalized[] = $entry;
        }

        $currentVersion = (int) ($template['version'] ?? 1);
        $newVersion = $currentVersion + 1;
        $schemaJson = json_encode(['version' => $newVersion, 'items' => $normalized], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if (!is_string($schemaJson)) {
            return ['ok' => false, 'errors' => ['schema' => t('Could not encode the checklist')]];
        }

        $ok = $this->repository->updateSchema($tenantId, $id, $schemaJson, $newVersion, $actorUserId);

        return $ok
            ? ['ok' => true, 'errors' => [], 'version' => $newVersion]
            : ['ok' => false, 'errors' => ['general' => t('Checklist could not be saved')]];
    }

    private static function slugify(string $text): string
    {
        $text = trim(mb_strtolower($text));
        $text = strtr($text, ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss']);
        $text = preg_replace('/[^a-z0-9]+/', '_', $text) ?? '';

        return trim($text, '_');
    }
}
