<?php

declare(strict_types=1);

final class MyronRailDesignRepository
{
    public const DEFAULT_RAIL = 'rail.blog.standard';

    private const VALID_RAILS = [
        self::DEFAULT_RAIL,
    ];

    private const VALID_PLACEMENTS = [
        'left',
        'right',
        'none',
    ];

    private const BLOG_TEMPLATE_IDS = [
        'template.blog-index.blog.v1',
        'template.post-single.blog.v1',
    ];

    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @return list<string>
     */
    public function validRails(): array
    {
        return self::VALID_RAILS;
    }

    /**
     * @return list<string>
     */
    public function validPlacements(): array
    {
        return self::VALID_PLACEMENTS;
    }

    /**
     * @return list<string>
     */
    public function supportedBlogTemplateIds(): array
    {
        return self::BLOG_TEMPLATE_IDS;
    }

    public function supportsTemplate(string $sourceTemplateId): bool
    {
        return in_array(trim($sourceTemplateId), self::BLOG_TEMPLATE_IDS, true);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listRails(): array
    {
        $this->ensureSeedRows();
        $statement = $this->pdo->query('SELECT * FROM rail_designs ORDER BY name ASC, rail_id ASC');
        $rows = $statement ? $statement->fetchAll(PDO::FETCH_ASSOC) : [];
        return array_values(array_map(fn (array $row): array => $this->hydrateRail($row), array_filter($rows, 'is_array')));
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listActivatedSorted(): array
    {
        return array_values(array_filter($this->listRails(), static fn (array $row): bool => (bool) ($row['is_activated'] ?? false)));
    }

    /**
     * @return array<string, mixed>
     */
    public function getDefault(): array
    {
        $this->ensureSeedRows();
        $statement = $this->pdo->query('SELECT * FROM rail_designs WHERE is_default = 1 LIMIT 1');
        $row = $statement ? $statement->fetch(PDO::FETCH_ASSOC) : false;
        if (is_array($row)) {
            return $this->hydrateRail($row);
        }

        $fallback = $this->getByRailId(self::DEFAULT_RAIL);
        $this->setDefault((string) $fallback['rail_id']);
        return $this->getByRailId((string) $fallback['rail_id']);
    }

    /**
     * @return array<string, mixed>
     */
    public function getByRailId(string $railId): array
    {
        $railId = $this->normalizeRailId($railId);
        $this->ensureSeedRows();
        $statement = $this->pdo->prepare('SELECT * FROM rail_designs WHERE rail_id = :rail_id LIMIT 1');
        $statement->execute([':rail_id' => $railId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        if (is_array($row)) {
            return $this->hydrateRail($row);
        }

        $this->insertRail($railId, $this->labelForRail($railId), true, $railId === self::DEFAULT_RAIL, 'right', $this->defaultContent());
        return $this->getByRailId($railId);
    }

    public function saveActivation(string $railId, bool $isActivated, string $userId = ''): void
    {
        $railId = $this->normalizeRailId($railId);
        $row = $this->getByRailId($railId);
        if (!$isActivated && (bool) ($row['is_default'] ?? false)) {
            throw new RuntimeException('Set a new default before deactivating this rail.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE rail_designs
             SET is_activated = :is_activated,
                 updated_by_user_id = :updated_by_user_id,
                 updated_at = :updated_at
             WHERE rail_id = :rail_id'
        );
        $statement->execute([
            ':is_activated' => $isActivated ? 1 : 0,
            ':updated_by_user_id' => $userId,
            ':updated_at' => myronNow(),
            ':rail_id' => $railId,
        ]);
    }

    public function setDefault(string $railId, string $userId = ''): void
    {
        $railId = $this->normalizeRailId($railId);
        $row = $this->getByRailId($railId);
        if (!(bool) ($row['is_activated'] ?? false)) {
            throw new RuntimeException('Only activated rails can be set as default.');
        }

        $now = myronNow();
        $this->pdo->beginTransaction();
        try {
            $this->pdo->exec('UPDATE rail_designs SET is_default = 0');
            $statement = $this->pdo->prepare(
                'UPDATE rail_designs
                 SET is_default = 1,
                     updated_by_user_id = :updated_by_user_id,
                     updated_at = :updated_at
                 WHERE rail_id = :rail_id'
            );
            $statement->execute([
                ':updated_by_user_id' => $userId,
                ':updated_at' => $now,
                ':rail_id' => $railId,
            ]);
            $this->pdo->commit();
        } catch (Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $exception;
        }
    }

    /**
     * @param array<string, mixed> $content
     * @return array<string, mixed>
     */
    public function saveConfiguration(string $railId, array $settings, string $userId = ''): array
    {
        $railId = $this->normalizeRailId($railId);
        $existing = $this->getByRailId($railId);
        $name = trim((string) ($settings['name'] ?? $existing['name'] ?? $this->labelForRail($railId)));
        $placementDefault = $this->normalizePlacement((string) ($settings['placement_default'] ?? $existing['placement_default'] ?? 'right'));
        $content = is_array($settings['content'] ?? null) ? $settings['content'] : (is_array($existing['content'] ?? null) ? $existing['content'] : []);

        if ($name === '') {
            throw new InvalidArgumentException('Rail name is required.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE rail_designs
             SET name = :name,
                 placement_default = :placement_default,
                 content_json = :content_json,
                 updated_by_user_id = :updated_by_user_id,
                 updated_at = :updated_at
             WHERE rail_id = :rail_id'
        );
        $statement->execute([
            ':name' => $name,
            ':placement_default' => $placementDefault,
            ':content_json' => json_encode($this->normalizeContent($content), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ':updated_by_user_id' => $userId,
            ':updated_at' => myronNow(),
            ':rail_id' => $railId,
        ]);

        return $this->getByRailId($railId);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getTemplateAssignment(string $sourceTemplateId): ?array
    {
        $sourceTemplateId = trim($sourceTemplateId);
        if ($sourceTemplateId === '') {
            return null;
        }
        $this->ensureSeedRows();
        $statement = $this->pdo->prepare('SELECT * FROM template_rail_assignments WHERE template_id = :template_id LIMIT 1');
        $statement->execute([':template_id' => $sourceTemplateId]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $this->hydrateAssignment($row) : null;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listTemplateAssignments(): array
    {
        $this->ensureSeedRows();
        $statement = $this->pdo->query('SELECT * FROM template_rail_assignments ORDER BY template_id ASC');
        $rows = $statement ? $statement->fetchAll(PDO::FETCH_ASSOC) : [];
        return array_values(array_map(fn (array $row): array => $this->hydrateAssignment($row), array_filter($rows, 'is_array')));
    }

    public function setTemplateAssignment(string $sourceTemplateId, ?string $railId, string $placement, string $userId = ''): void
    {
        $sourceTemplateId = trim($sourceTemplateId);
        if ($sourceTemplateId === '') {
            throw new InvalidArgumentException('Template id is required.');
        }
        if (!$this->supportsTemplate($sourceTemplateId)) {
            throw new InvalidArgumentException('This Page Template does not support a rail.');
        }

        $normalizedRailId = $railId === null || trim($railId) === '' ? self::DEFAULT_RAIL : $this->normalizeRailId($railId);
        $normalizedPlacement = $this->normalizePlacement($placement);
        $this->getByRailId($normalizedRailId);

        $existing = $this->getTemplateAssignment($sourceTemplateId);
        $now = myronNow();
        if (is_array($existing)) {
            $statement = $this->pdo->prepare(
                'UPDATE template_rail_assignments
                 SET rail_id = :rail_id,
                     placement = :placement,
                     updated_by_user_id = :updated_by_user_id,
                     updated_at = :updated_at
                 WHERE template_id = :template_id'
            );
            $statement->execute([
                ':template_id' => $sourceTemplateId,
                ':rail_id' => $normalizedRailId,
                ':placement' => $normalizedPlacement,
                ':updated_by_user_id' => $userId,
                ':updated_at' => $now,
            ]);
            return;
        }

        $statement = $this->pdo->prepare(
            'INSERT INTO template_rail_assignments (
                id, template_id, rail_id, placement, created_at, updated_at, updated_by_user_id
             ) VALUES (
                :id, :template_id, :rail_id, :placement, :created_at, :updated_at, :updated_by_user_id
             )'
        );
        $statement->execute([
            ':id' => 'tra_' . substr(sha1($sourceTemplateId), 0, 12),
            ':template_id' => $sourceTemplateId,
            ':rail_id' => $normalizedRailId,
            ':placement' => $normalizedPlacement,
            ':created_at' => $now,
            ':updated_at' => $now,
            ':updated_by_user_id' => $userId,
        ]);
    }

    /**
     * @param array<string, mixed> $schema
     * @return array{rail_id:string,rail_name:string,placement:string,source:string}
     */
    public function resolveForTemplate(string $sourceTemplateId, array $schema = []): array
    {
        if (!$this->supportsTemplate($sourceTemplateId)) {
            return [
                'rail_id' => '',
                'rail_name' => '',
                'placement' => 'none',
                'source' => 'unsupported',
            ];
        }

        $assignment = $this->getTemplateAssignment($sourceTemplateId);
        if (is_array($assignment)) {
            $rail = $this->getByRailId((string) ($assignment['rail_id'] ?? self::DEFAULT_RAIL));
            return [
                'rail_id' => (string) ($rail['rail_id'] ?? self::DEFAULT_RAIL),
                'rail_name' => (string) ($rail['name'] ?? 'Blog Rail'),
                'placement' => $this->normalizePlacement((string) ($assignment['placement'] ?? $rail['placement_default'] ?? 'right')),
                'source' => 'assignment',
            ];
        }

        $rail = $this->getDefault();
        $legacyRail = is_array($schema['blogRail'] ?? null) ? $schema['blogRail'] : [];
        $legacyPlacement = myronNormalizeSlug((string) ($legacyRail['placement'] ?? ''));
        $placement = in_array($legacyPlacement, self::VALID_PLACEMENTS, true)
            ? $legacyPlacement
            : $this->normalizePlacement((string) ($rail['placement_default'] ?? 'right'));

        return [
            'rail_id' => (string) ($rail['rail_id'] ?? self::DEFAULT_RAIL),
            'rail_name' => (string) ($rail['name'] ?? 'Blog Rail'),
            'placement' => $placement,
            'source' => $legacyPlacement !== '' ? 'legacy-blogRail' : 'default',
        ];
    }

    public function normalizePlacement(string $placement): string
    {
        $placement = myronNormalizeSlug($placement);
        if (!in_array($placement, self::VALID_PLACEMENTS, true)) {
            throw new InvalidArgumentException('Rail placement must be left, right, or none.');
        }
        return $placement;
    }

    public function normalizeRailId(string $railId): string
    {
        $railId = trim($railId);
        if ($railId === '') {
            throw new InvalidArgumentException('Rail id is required.');
        }
        if (!in_array($railId, self::VALID_RAILS, true)) {
            throw new InvalidArgumentException('Unknown rail: ' . $railId);
        }
        return $railId;
    }

    private function ensureSeedRows(): void
    {
        $count = (int) ($this->pdo->query('SELECT COUNT(*) FROM rail_designs')?->fetchColumn() ?: 0);
        if ($count === 0) {
            $this->insertRail(self::DEFAULT_RAIL, 'Standard Blog Rail', true, true, 'right', $this->defaultContent());
        }

        foreach (self::BLOG_TEMPLATE_IDS as $templateId) {
            $this->setTemplateAssignmentIfMissing($templateId, self::DEFAULT_RAIL, 'right');
        }
    }

    /**
     * @param array<string, mixed> $content
     */
    private function insertRail(string $railId, string $name, bool $activated, bool $default, string $placementDefault, array $content): void
    {
        $now = myronNow();
        $statement = $this->pdo->prepare(
            'INSERT INTO rail_designs (
                id, rail_id, name, context_key, is_activated, is_default, placement_default,
                content_json, created_at, updated_at, created_by_user_id, updated_by_user_id
             )
             SELECT :id, :rail_id, :name, :context_key, :is_activated, :is_default, :placement_default,
                    :content_json, :created_at, :updated_at, "", ""
             WHERE NOT EXISTS (SELECT 1 FROM rail_designs WHERE rail_id = :rail_id_existing)'
        );
        $statement->execute([
            ':id' => 'rd_' . str_replace(['rail.', '-'], ['', '_'], $railId),
            ':rail_id' => $railId,
            ':rail_id_existing' => $railId,
            ':name' => $name,
            ':context_key' => 'blog',
            ':is_activated' => $activated ? 1 : 0,
            ':is_default' => $default ? 1 : 0,
            ':placement_default' => $this->normalizePlacement($placementDefault),
            ':content_json' => json_encode($this->normalizeContent($content), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
    }

    private function setTemplateAssignmentIfMissing(string $sourceTemplateId, string $railId, string $placement): void
    {
        $statement = $this->pdo->prepare('SELECT COUNT(*) FROM template_rail_assignments WHERE template_id = :template_id');
        $statement->execute([':template_id' => $sourceTemplateId]);
        if ((int) $statement->fetchColumn() > 0) {
            return;
        }

        $now = myronNow();
        $insert = $this->pdo->prepare(
            'INSERT INTO template_rail_assignments (
                id, template_id, rail_id, placement, created_at, updated_at, updated_by_user_id
             ) VALUES (
                :id, :template_id, :rail_id, :placement, :created_at, :updated_at, ""
             )'
        );
        $insert->execute([
            ':id' => 'tra_' . substr(sha1($sourceTemplateId), 0, 12),
            ':template_id' => $sourceTemplateId,
            ':rail_id' => $railId,
            ':placement' => $placement,
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function hydrateRail(array $row): array
    {
        $railId = (string) ($row['rail_id'] ?? self::DEFAULT_RAIL);
        return [
            'id' => (string) ($row['id'] ?? ''),
            'rail_id' => $railId,
            'name' => (string) ($row['name'] ?? $this->labelForRail($railId)),
            'context_key' => (string) ($row['context_key'] ?? 'blog'),
            'is_activated' => (bool) ((int) ($row['is_activated'] ?? 0)),
            'is_default' => (bool) ((int) ($row['is_default'] ?? 0)),
            'placement_default' => $this->safePlacement((string) ($row['placement_default'] ?? 'right')),
            'content' => $this->decodeContent((string) ($row['content_json'] ?? '{}')),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function hydrateAssignment(array $row): array
    {
        return [
            'id' => (string) ($row['id'] ?? ''),
            'template_id' => (string) ($row['template_id'] ?? ''),
            'rail_id' => (string) ($row['rail_id'] ?? self::DEFAULT_RAIL),
            'placement' => $this->safePlacement((string) ($row['placement'] ?? 'right')),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
            'updated_by_user_id' => (string) ($row['updated_by_user_id'] ?? ''),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function defaultContent(): array
    {
        return [
            'summary' => 'Core Blog sidebar rail for subscription, discovery, and related-content widgets.',
            'slots' => [
                ['key' => 'subscribe', 'label' => 'Newsletter signup', 'componentId' => 'newsletter-signup'],
                ['key' => 'discover', 'label' => 'Categories and discovery', 'componentId' => 'stack'],
                ['key' => 'related', 'label' => 'Related posts', 'componentId' => 'blog-teaser'],
            ],
        ];
    }

    private function labelForRail(string $railId): string
    {
        return $railId === self::DEFAULT_RAIL ? 'Standard Blog Rail' : ucwords(str_replace(['rail.', '-'], ['', ' '], $railId));
    }

    private function safePlacement(string $placement): string
    {
        $placement = myronNormalizeSlug($placement);
        return in_array($placement, self::VALID_PLACEMENTS, true) ? $placement : 'right';
    }

    /**
     * @param array<string, mixed> $content
     * @return array<string, mixed>
     */
    private function normalizeContent(array $content): array
    {
        $summary = trim((string) ($content['summary'] ?? $this->defaultContent()['summary']));
        $slots = [];
        foreach ((array) ($content['slots'] ?? []) as $slot) {
            if (!is_array($slot)) {
                continue;
            }
            $key = myronNormalizeSlug((string) ($slot['key'] ?? ''));
            $label = trim((string) ($slot['label'] ?? ''));
            $componentId = trim((string) ($slot['componentId'] ?? ''));
            if ($key === '' || $label === '') {
                continue;
            }
            $slots[] = [
                'key' => $key,
                'label' => $label,
                'componentId' => $componentId,
            ];
        }
        if ($slots === []) {
            $slots = $this->defaultContent()['slots'];
        }

        return [
            'summary' => $summary,
            'slots' => $slots,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeContent(string $raw): array
    {
        $decoded = json_decode($raw !== '' ? $raw : '{}', true);
        return is_array($decoded) ? $this->normalizeContent($decoded) : $this->defaultContent();
    }
}
