<?php

namespace Common\Core\Prerender;

use Arr;
use Common\Core\Contracts\AppUrlGenerator;
use Common\Core\Prerender\Actions\ReplacePlaceholders;
use Common\Settings\Settings;
use Illuminate\Contracts\Support\Arrayable;
use Str;

class MetaTags implements Arrayable
{
    /**
     * Tag types that can be edited by the user.
     */
    const EDITABLE_TAGS = ['og:title', 'og:description', 'keywords'];

    /**
     * Data for replacing meta tag config placeholders.
     */
    protected array $data = [];

    /**
     * Meta tag config before generation.
     */
    protected array $tags = [];

    /**
     * Final tags for appending to site head.
     */
    protected array $generatedTags = [];

    /**
     * Namespace for current tag config. "artist.show".
     *
     * @var string
     */
    protected string $namespace;

    public AppUrlGenerator $urls;

    private Settings $settings;

    public function __construct($tags, $data, $namespace)
    {
        $this->namespace = $namespace;
        $this->data = $data;
        $tags = $this->overrideTagsWithUserValues($tags);
        $this->tags = array_merge($tags, config('seo.common'));
        $this->urls = app(AppUrlGenerator::class);
        $this->settings = app(Settings::class);
        $this->generatedTags = $this->generateTags();
    }

    public function toArray()
    {
        // remove all tags to which placeholders could not be replaced with actual content
        $tags = collect($this->getAll())->map(function ($tag) {
            // ld+json tags will contain child arrays
            $strings = array_filter($tag, function ($value) {
                return !is_array($value);
            });
            $content = implode($strings);
            $shouldRemove =
                Str::contains($content, '{{') && Str::contains($content, '}}');

            // if could not replace title placeholder, return app name as title instead
            if ($shouldRemove && $tag['nodeName'] === 'title') {
                $tag['_text'] = config('app.name');
                $shouldRemove = false;
            }
            if ($shouldRemove && Arr::get($tag, 'property') === 'og:title') {
                $tag['content'] = config('app.name');
                $shouldRemove = false;
            }

            return $shouldRemove ? null : $tag;
        });

        return $tags
            ->filter()
            ->values()
            ->toArray();
    }

    public function getTitle()
    {
        return $this->get('og:title');
    }

    public function getDescription()
    {
        return $this->get('og:description');
    }

    public function getImage()
    {
        return url($this->get('og:image'));
    }

    public function getMenu(string $position): array
    {
        $default = ['items' => []];
        return Arr::first(
            settings('menus'),
            fn($menu) => in_array($position, $menu['positions']),
            $default,
        );
    }

    public function get($value, $prop = 'property')
    {
        $tag = Arr::first(
            $this->generatedTags,
            function ($tag) use ($prop, $value) {
                return Arr::get($tag, $prop) === $value;
            },
            [],
        );

        return Arr::get($tag, 'content');
    }

    public function getData($selector = null, $default = null)
    {
        return Arr::get($this->data, $selector) ?? $default;
    }

    public function getAll(): array
    {
        return $this->generatedTags;
    }

    /**
     * Convert specified tag config into a string.
     *
     * @param array $tag
     * @return string
     */
    public function tagToString($tag)
    {
        $string = '';

        foreach (Arr::except($tag, 'nodeName') as $key => $value) {
            $value = is_array($value) ? implode(',', $value) : $value;
            $string .= "$key=\"$value\" ";
        }

        return trim($string);
    }

    private function generateTags(): array
    {
        $tags = $this->tags;

        $tags = array_map(function ($tag) {
            return $this->replacePlaceholders($tag);
        }, $tags);
        $tags = $this->removeEmptyValues($tags);
        $tags = $this->duplicateTags($tags);
        $tags = $this->escapeValues($tags);

        $tags = array_map(function ($tag) {
            // set nodeName to <meta> tag, if not already specified
            if (!array_key_exists('nodeName', $tag)) {
                $tag['nodeName'] = 'meta';
            }
            return $tag;
        }, $tags);

        return $tags;
    }

    function removeEmptyValues(array &$array): array
    {
        foreach ($array as $key => &$value) {
            if (is_array($value)) {
                $value = $this->removeEmptyValues($value);
            }
            if (empty($value)) {
                unset($array[$key]);
            }
        }
        return $array;
    }

    private function replacePlaceholders(array $tag): array
    {
        $replacer = app(ReplacePlaceholders::class);

        // if tag does not have "content" or "_text" prop, we can continue
        if (array_key_exists('content', $tag)) {
            $tag['content'] = $replacer->execute($tag['content'], $this->data);
        } elseif (array_key_exists('_text', $tag)) {
            $tag['_text'] = $replacer->execute($tag['_text'], $this->data);
        }

        return $tag;
    }

    private function escapeValues(array $tags): array
    {
        // only escape meta tags that have "content" property
        foreach ($tags as $key => $tag) {
            if (array_key_exists('content', $tag)) {
                $tags[$key]['content'] = str_replace(
                    '"',
                    '&quot;',
                    $tag['content'],
                );
            }
        }

        return $tags;
    }

    /**
     * Create duplicate tags from generated tags.
     * (for example: canonical link from og:url)
     *
     * @param array $tags
     * @return array
     */
    private function duplicateTags($tags)
    {
        foreach ($tags as $tag) {
            if (!isset($tag['content'])) {
                continue;
            }

            if (Arr::get($tag, 'property') === 'og:url') {
                $tags[] = [
                    'nodeName' => 'link',
                    'rel' => 'canonical',
                    'href' => $tag['content'],
                ];
            }

            if (Arr::get($tag, 'property') === 'og:title') {
                $tags[] = [
                    'nodeName' => 'title',
                    '_text' => ucfirst($tag['content']),
                ];
            }

            if (Arr::get($tag, 'property') === 'og:description') {
                $tags[] = [
                    'name' => 'description',
                    'content' => $tag['content'],
                ];
            }
        }

        return $tags;
    }

    private function overrideTagsWithUserValues(array $metaTags): array
    {
        $overrides = app(Settings::class)->all();
        foreach ($metaTags as $key => $tagConfig) {
            $property = Arr::get($tagConfig, 'property');
            $settingKey = "seo.{$this->namespace}.{$property}";
            if (
                in_array($property, self::EDITABLE_TAGS) &&
                array_key_exists($settingKey, $overrides)
            ) {
                $metaTags[$key]['content'] = $overrides[$settingKey];
            }
        }

        return $metaTags;
    }
}
