<?php
declare(strict_types=1);

namespace Batoi\Press\Content;

use Batoi\Press\Core\FileStore;
use Batoi\Press\Core\HtmlContent;
use Batoi\Press\Core\Paths;
use Batoi\Press\Core\Slug;
use RuntimeException;

final class PostRepository
{
    public function __construct(
        private readonly Paths $paths,
        private readonly FileStore $files,
        private readonly HtmlContent $html
    ) {
    }

    public function findBySlug(string $slug): ?array
    {
        foreach ($this->all() as $post) {
            if (($post['slug'] ?? '') === $slug) {
                return $post;
            }
        }

        return null;
    }

    public function allPublished(): array
    {
        return array_values(array_filter($this->all(), static fn (array $post): bool => PublicationState::isPublic($post)));
    }

    public function all(): array
    {
        $base = $this->paths->contentPath('posts');
        if (!is_dir($base)) {
            return [];
        }

        $items = [];
        foreach (glob($base . '/*', GLOB_ONLYDIR) ?: [] as $dir) {
            $metaFile = $dir . '/meta.json';
            $bodyFile = $dir . '/body.html';
            if (!is_file($metaFile) || !is_file($bodyFile)) {
                continue;
            }

            $meta = $this->files->readJson($metaFile);
            $meta['body'] = $this->html->sanitize($this->files->read($bodyFile));
            $items[] = $meta;
        }

        usort($items, static fn (array $a, array $b): int => strcmp((string)($b['publish_at'] ?? $b['published_at'] ?? ''), (string)($a['publish_at'] ?? $a['published_at'] ?? '')));
        return $items;
    }

    public function save(array $input, string $actor): array
    {
        $requestedSlug = trim((string)($input['slug'] ?? ''));
        $slug = Slug::normalize($requestedSlug !== '' ? $requestedSlug : (string)($input['title'] ?? ''));
        if ($slug === '') {
            $slug = 'post-' . date('Ymd-His');
        }

        $originalSlug = Slug::normalize((string)($input['original_slug'] ?? ''));
        $now = date(DATE_ATOM);
        $existing = $originalSlug !== '' ? $this->findBySlug($originalSlug) : $this->findBySlug($slug);
        if ($originalSlug !== '' && $originalSlug !== $slug && $this->findBySlug($slug) !== null) {
            throw new RuntimeException('A post with this slug already exists.');
        }
        $status = PublicationState::normalize($input['status'] ?? 'draft');
        $requestedPublishAt = $input['publish_at'] ?? $input['published_at'] ?? $existing['publish_at'] ?? $existing['published_at'] ?? '';
        $publishAt = PublicationState::normalizeDate($requestedPublishAt, 'Publish');
        if ($status === 'published' && $publishAt === '') {
            $publishAt = $now;
        }
        if ($status === 'scheduled' && $publishAt === '') {
            throw new RuntimeException('A scheduled post requires a publish date and time.');
        }
        $unpublishAt = PublicationState::normalizeDate($input['unpublish_at'] ?? $existing['unpublish_at'] ?? '', 'Unpublish');
        $reviewer = substr(trim((string)($input['reviewer'] ?? $existing['reviewer'] ?? '')), 0, 100);
        $workflowNote = substr(trim((string)($input['workflow_note'] ?? '')), 0, 500);
        $layout = (string)($input['layout'] ?? $existing['layout'] ?? 'full');
        $meta = [
            'id' => (string)($existing['id'] ?? 'post_' . bin2hex(random_bytes(6))),
            'type' => 'post',
            'title' => trim((string)($input['title'] ?? 'Untitled Post')),
            'subtitle' => trim((string)($input['subtitle'] ?? '')),
            'slug' => $slug,
            'status' => $status,
            'publish_at' => $publishAt,
            'unpublish_at' => $unpublishAt,
            'reviewer' => $reviewer,
            'workflow_history' => PublicationState::history($existing ?? [], $status, $reviewer, $workflowNote, $actor, $now),
            'template' => 'post',
            'author' => (string)($existing['author'] ?? $actor),
            'category' => trim((string)($input['category'] ?? 'General')),
            'featured_image' => $this->safeAssetUrl((string)($input['featured_image'] ?? '')),
            'featured_image_alt' => trim((string)($input['featured_image_alt'] ?? '')),
            'layout' => in_array($layout, ['full', 'sidebar-right', 'sidebar-left'], true) ? $layout : 'full',
            'tags' => array_values(array_filter(array_map('trim', explode(',', (string)($input['tags'] ?? ''))))),
            'created_at' => (string)($existing['created_at'] ?? $now),
            'updated_at' => $now,
            'published_at' => $publishAt,
            'seo_title' => trim((string)($input['seo_title'] ?? $input['title'] ?? '')),
            'seo_description' => trim((string)($input['seo_description'] ?? '')),
        ];

        $dir = $this->targetDir($originalSlug, $slug);
        $this->snapshot($dir, $slug);
        $this->files->writeJson($dir . '/meta.json', $meta);
        $this->files->write($dir . '/body.html', $this->html->sanitize((string)($input['body'] ?? '')));

        return $meta;
    }

    public function adjacentPublished(string $slug): array
    {
        $posts = $this->allPublished();
        foreach ($posts as $index => $post) {
            if ((string)($post['slug'] ?? '') !== $slug) {
                continue;
            }

            return [
                'previous' => $posts[$index + 1] ?? null,
                'next' => $index > 0 ? $posts[$index - 1] : null,
            ];
        }

        return ['previous' => null, 'next' => null];
    }

    private function safeAssetUrl(string $value): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }
        if (str_starts_with($value, '/') && !str_starts_with($value, '//')) {
            return $value;
        }
        return filter_var($value, FILTER_VALIDATE_URL) !== false && preg_match('#^https?://#i', $value) === 1 ? $value : '';
    }

    private function targetDir(string $originalSlug, string $slug): string
    {
        $dir = $this->paths->contentPath('posts/' . $slug);
        if ($originalSlug === '' || $originalSlug === $slug) {
            return $dir;
        }

        $source = $this->paths->contentPath('posts/' . $originalSlug);
        if (!is_dir($source)) {
            return $dir;
        }

        $this->snapshot($source, $originalSlug);
        if (is_dir($dir)) {
            throw new RuntimeException('Unable to rename post because the target slug directory already exists.');
        }
        if (!rename($source, $dir)) {
            throw new RuntimeException('Unable to rename post content directory.');
        }

        return $dir;
    }

    private function snapshot(string $dir, string $slug): void
    {
        if (!is_file($dir . '/meta.json') && !is_file($dir . '/body.html')) {
            return;
        }

        $target = $this->paths->dataPath('versions/posts/' . $slug . '/' . date('Ymd-His'));
        if (is_file($dir . '/meta.json')) {
            $this->files->write($target . '/meta.json', $this->files->read($dir . '/meta.json'));
        }
        if (is_file($dir . '/body.html')) {
            $this->files->write($target . '/body.html', $this->files->read($dir . '/body.html'));
        }
    }
}
