<?php

declare(strict_types=1);

namespace App\Domain\Blog;

use App\Models\Attachment;
use Illuminate\Support\Facades\Cache;

/**
 * Attachment embeds:
 * - Legacy HTML: [attach=123] or src/href="[attach=123]"
 * - Markdown-safe: ![alt](attach:123) or [label](attach:123)
 *
 * DB stores references only; URLs are resolved at render/preview time.
 */
class AttachEmbed
{
    public const LEGACY_PATTERN = '/\[attach=(\d+)\]/i';

    public const MD_PROTOCOL = 'attach:';

    public function legacyToMarkdownReference(string $content, ?int $articleId = null): string
    {
        return preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use ($articleId): string {
            $id = (int) $matches[1];
            $attachment = $this->findAttachment($id, $articleId);

            $name = $attachment?->filename ?: "attachment-{$id}";
            $alt = pathinfo($name, PATHINFO_FILENAME) ?: $name;

            if ($attachment && $this->isImage($attachment)) {
                return '!['.$this->escapeMdLabel($alt).']('.self::MD_PROTOCOL.$id.')';
            }

            return '['.$this->escapeMdLabel($name).']('.self::MD_PROTOCOL.$id.')';
        }, $content) ?? $content;
    }

    /**
     * Protect sablog attach tokens before HTML→Markdown conversion.
     * Tokens avoid underscores so html-to-markdown won't escape them.
     *
     * @return array{0: string, 1: array<string, array{id:int, kind:string, role:string}>}
     */
    public function protectLegacyTokensForHtmlConversion(string $html, ?int $articleId = null): array
    {
        $map = [];

        // Attribute values become markdown link/image destinations → restore as attach:ID only.
        $html = preg_replace_callback(
            '/\b(src|href)=([\'"])\[attach=(\d+)\]\2/i',
            function (array $matches) use (&$map, $articleId): string {
                $id = (int) $matches[3];
                $role = 'dest';
                $kind = strtolower($matches[1]) === 'src' ? 'image' : 'file';
                $token = 'LBATTACHDEST'.$id.'X';
                $map[$token] = ['id' => $id, 'kind' => $kind, 'role' => $role, 'article_id' => $articleId];

                return $matches[1].'='.$matches[2].$token.$matches[2];
            },
            $html
        ) ?? $html;

        // Bare tokens become full markdown embeds after conversion.
        $html = preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use (&$map, $articleId): string {
            $id = (int) $matches[1];
            $attachment = $this->findAttachment($id, $articleId);
            $kind = ($attachment && $this->isImage($attachment)) ? 'image' : 'file';
            $token = 'LBATTACHBARE'.$id.'X';
            $map[$token] = ['id' => $id, 'kind' => $kind, 'role' => 'bare', 'article_id' => $articleId];

            return $token;
        }, $html) ?? $html;

        return [$html, $map];
    }

    /**
     * @param  array<string, array{id:int, kind:string, role:string}>  $map
     */
    public function restoreProtectedTokensToMarkdown(string $markdown, array $map): string
    {
        foreach ($map as $token => $meta) {
            $id = (int) $meta['id'];

            if (($meta['role'] ?? '') === 'dest') {
                $markdown = str_replace($token, self::MD_PROTOCOL.$id, $markdown);

                continue;
            }

            $attachment = $this->findAttachment($id, $meta['article_id'] ?? null);
            $name = $attachment?->filename ?: "attachment-{$id}";
            $alt = pathinfo($name, PATHINFO_FILENAME) ?: $name;

            $replacement = ($meta['kind'] ?? 'file') === 'image'
                ? '!['.$this->escapeMdLabel($alt).']('.self::MD_PROTOCOL.$id.')'
                : '['.$this->escapeMdLabel($name).']('.self::MD_PROTOCOL.$id.')';

            $markdown = str_replace($token, $replacement, $markdown);
        }

        return $markdown;
    }

    /**
     * Render-time only: turn attach:123 into /attachment.php?id=123 for CommonMark.
     */
    public function expandMarkdownAttachProtocol(string $markdown): string
    {
        return preg_replace_callback(
            '/\]\(attach:(\d+)\)/i',
            fn (array $matches): string => ']('.$this->publicEntryUrl((int) $matches[1]).')',
            $markdown
        ) ?? $markdown;
    }

    public function hydrateHtml(string $html, ?int $articleId = null): string
    {
        // 1) Attribute form: src/href="[attach=N]"  (must run before bare token replace)
        $html = preg_replace_callback(
            '/\b(src|href)=([\'"])\[attach=(\d+)\]\2/i',
            function (array $matches): string {
                $attr = strtolower($matches[1]);
                $quote = $matches[2];
                $id = (int) $matches[3];

                return $attr.'='.$quote.$this->publicEntryUrl($id).$quote;
            },
            $html
        ) ?? $html;

        // 2) Safety net for attach: protocol left in HTML attributes
        $html = preg_replace_callback(
            '/\b(href|src)=([\'"])attach:(\d+)\2/i',
            function (array $matches): string {
                $attr = strtolower($matches[1]);
                $quote = $matches[2];
                $id = (int) $matches[3];

                return $attr.'='.$quote.$this->publicEntryUrl($id).$quote;
            },
            $html
        ) ?? $html;

        // 3) Bare legacy [attach=id] tokens
        $html = preg_replace_callback(self::LEGACY_PATTERN, function (array $matches) use ($articleId): string {
            $id = (int) $matches[1];
            $attachment = $this->findAttachment($id, $articleId);
            $url = $this->publicEntryUrl($id);

            if ($attachment && $this->isImage($attachment)) {
                $alt = e(pathinfo($attachment->filename, PATHINFO_FILENAME) ?: $attachment->filename);

                return '<img src="'.e($url).'" alt="'.$alt.'" />';
            }

            $label = e($attachment?->filename ?: "attachment-{$id}");

            return '<a href="'.e($url).'">'.$label.'</a>';
        }, $html) ?? $html;

        return $html;
    }

    public function publicEntryUrl(int $attachmentId): string
    {
        return url('/attachment.php?id='.$attachmentId);
    }

    protected function findAttachment(int $id, ?int $articleId = null): ?Attachment
    {
        try {
            return Cache::remember("attach-embed:{$id}", 60, function () use ($id) {
                return Attachment::query()->find($id);
            });
        } catch (\Throwable) {
            return null;
        }
    }

    protected function isImage(Attachment $attachment): bool
    {
        if (is_string($attachment->mime) && str_starts_with($attachment->mime, 'image/')) {
            return true;
        }

        $ext = strtolower(pathinfo($attachment->filename, PATHINFO_EXTENSION));

        return in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'], true);
    }

    protected function escapeMdLabel(string $label): string
    {
        return str_replace(['[', ']', '!'], ['\\[', '\\]', '\\!'], $label);
    }
}
