<?php

namespace Bongo\Builder\Models;

use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\Finder\SplFileInfo;

class BuilderItem
{
    public string $type;

    public ?string $name = null;

    public ?string $key = null;

    public ?string $category = null;

    public ?string $html = null;

    public ?string $thumbnail = null;

    public ?string $relativePath = null;

    public ?string $relativeHtmlPath = null;

    public ?string $relativeThumbnailPath = null;

    public ?string $absolutePath = null;

    public ?string $absoluteHtmlPath = null;

    public ?string $absoluteThumbnailPath = null;

    public function __construct(string $type)
    {
        $this->type = $type;
    }

    /**
     * @throws Exception
     */
    public function all(): Collection
    {
        return Cache::remember($this->type.'_files', 600, function () {

            // Create the collection
            $items = new Collection();

            // If we have files then add them to the collection
            if ($files = $this->getAllFiles()) {

                // Loop through the files
                foreach ($files as $file) {
                    try {
                        // Create the item and push the item into the collection
                        $item = $this->createFromFile($file);
                        $items->push($item);

                    } catch (Exception $e) {
                        log_exception($e);
                        continue;
                    }
                }
            }

            return $this->filterCustomItems($items);
        });
    }

    protected function filterCustomItems(Collection $items): Collection
    {
        $customItems = $items->where('category', 'custom');

        if ($customItems->isEmpty()) {
            return $items;
        }

        foreach ($customItems as $customItem) {
            if ($this->shouldExcludeCustomItem($customItem)) {
                $items = $items->reject(function ($item) use ($customItem) {
                    return $item->key === $customItem->key;
                });
            }
        }

        return $items;
    }

    protected function shouldExcludeCustomItem(self $customItem): bool
    {
        $package = $this->getRequiredPackageForCustomItem($customItem->key);

        return ! empty($package) && ! package()->isEnabled($package);
    }

    protected function getRequiredPackageForCustomItem(?string $itemKey): ?string
    {
        $packageMap = [
            'estimate' => 'estimate',
            'post' => 'post',
            'project' => 'project',
            'question' => 'question',
            'review' => 'review',
            'review_form' => 'review',
            'review_summary' => 'review',
        ];

        return $packageMap[$itemKey] ?? null;
    }

    /**
     * @throws Exception
     */
    public function allByCategory(): array
    {
        $categories = [];
        foreach ($this->all() as $item) {
            $categories["{$item->category}"]["{$item->key}"] = $item;
        }
        ksort($categories);

        return $categories;
    }

    /**
     * @throws Exception
     */
    public function allAsJson(): false|string
    {
        $components = $this->all();

        $jsonComponents = [];
        foreach ($components as $component) {
            $jsonComponents[] = [
                'name' => $component->getName(),
                'thumbnail' => $component->getThumbnail(),
                'category' => $component->getCategory(),
                'html' => preg_replace('/[\t\s\n]*(<.*>)[\t\s\n]*/', '$1', $component->getHtml()),
            ];
        }

        return json_encode($jsonComponents);
    }

    public function categories(): array
    {
        $categories = config("builder.{$this->type}_categories");
        sort($categories);

        $categoryArray = [];
        foreach ($categories as $key => $category) {
            $categoryArray[trim(strtolower(Str::slug($category)))] = $category;
        }

        return $categoryArray;
    }

    public function categoriesAsJson(): false|string
    {
        $categories = config("builder.{$this->type}_categories");
        sort($categories);

        $categoryArray = [];
        foreach ($categories as $key => $category) {
            $categoryArray[$key]['name'] = $category;
            $categoryArray[$key]['key'] = trim(strtolower(Str::slug($category)));
        }

        return json_encode($categoryArray);
    }

    /**
     * @throws Exception
     */
    public function find(string $key): mixed
    {
        return $this->all()->firstWhere('key', $key);
    }

    /**
     * @return mixed|void
     * @throws Exception
     */
    public function findOrFail(string $key)
    {
        return $this->find($key) ?? abort('404');
    }

    /**
     * @throws Exception
     */
    public function findByCategory(string $category): Collection
    {
        return $this->all()->where('category', $category);
    }

    /**
     * @throws Exception
     */
    public function save(): void
    {
        // Save the changes to the override location
        if (empty($this->getAbsolutePath()) || ! $this->isExtended()) {
            $this->setAbsolutePath($this->getResourcePath().$this->getCategory().DIRECTORY_SEPARATOR.$this->getName());
            $this->setAbsoluteHtmlPath();
            $this->setAbsoluteThumbnailPath();
        }

        // If the component directory does not exist then create it
        if (! File::exists($this->getAbsolutePath())) {
            File::makeDirectory($this->getAbsolutePath(), 0755, true);
        }

        // Add the preview.png
        File::put($this->getAbsoluteThumbnailPath(), $this->thumbnail);

        // Add the index.blade.php file
        File::put($this->getAbsoluteHtmlPath(), $this->html);

        // Clear the cache
        Cache::forget($this->type.'_files');
    }

    /**
     * @throws Exception
     */
    public function delete(): void
    {
        // If we have a modified version of this file then delete it
        $resourceComponent = $this->getResourcePath().$this->getCategory().DIRECTORY_SEPARATOR.$this->getName();
        if (File::exists($resourceComponent)) {
            File::deleteDirectory($resourceComponent);
            Cache::forget($this->type.'_files');
        }
    }

    /**
     * @throws Exception
     */
    public function createFromFile(SplFileInfo $file): BuilderItem
    {
        // Get the category / component name
        $segments = explode(DIRECTORY_SEPARATOR, $file->getRelativePath());
        $category = $segments[0];
        $name = $segments[1];

        // Create the item
        $item = new self($this->type);
        $item->setName($name);
        $item->setKey($name);
        $item->setCategory($category);
        $item->setRelativePath($file->getRelativePath());
        $item->setRelativeHtmlPath();
        $item->setRelativeThumbnailPath();
        $item->setAbsolutePath($file->getPath());
        $item->setAbsoluteHtmlPath();
        $item->setAbsoluteThumbnailPath();

        return $item;
    }

    /**
     * The base path directory contains the unmodified package files
     *
     * @return string
     * @throws Exception
     */
    public function getBasePath(): string
    {
        // The location of the components directory
        $basePath = base_path(config('builder.vendor_path').config('builder.'.$this->type.'_path'));

        // Make sure the directory exists
        if (! File::exists($basePath)) {
            throw new Exception("Directory {$basePath} does not exist");
        }

        return $basePath;
    }

    /**
     * The resource directory contains files that have been overridden
     *
     * @return string
     * @throws Exception
     */
    public function getResourcePath(): string
    {
        // The location of the components directory
        $resourcePath = base_path(config('builder.resource_path').config('builder.'.$this->type.'_path'));

        // Make sure the directory exists
        if (! File::exists($resourcePath)) {
            File::makeDirectory($resourcePath, 0755, true); // Must be re-cursive due to long path
        }

        return $resourcePath;
    }

    /**
     * Get the files from the vendor directory
     * and replace with any that have been overridden
     *
     * @return array
     * @throws Exception
     */
    public function getAllFiles(): array
    {
        return array_replace(
            $this->getVendorFiles(),
            $this->getResourceFiles()
        );
    }

    /**
     * @return array
     * @throws Exception
     */
    public function getVendorFiles(): array
    {
        $vendorFiles = [];

        // Loop through the files and key by name
        if ($files = File::allFiles($this->getBasePath())) {
            foreach ($files as $file) {
                if ($file->getExtension() == 'php') {
                    $vendorFiles[$file->getRelativePath()] = $file;
                }
            }
        }

        return $vendorFiles;
    }

    /**
     * @return SplFileInfo[]
     * @throws Exception
     */
    public function getResourceFiles(): array
    {
        $resourceFiles = [];

        // Loop through the files and key by name
        if ($files = File::allFiles($this->getResourcePath())) {
            foreach ($files as $file) {
                if ($file->getExtension() == 'php') {
                    $resourceFiles[$file->getRelativePath()] = $file;
                }
            }
        }

        return $resourceFiles;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $value): void
    {
        $this->name = trim(strtolower(Str::slug($value)));
    }

    public function getKey(): ?string
    {
        return $this->key;
    }

    public function setKey(string $value): void
    {
        $this->key = make_key($value);
    }

    public function getCategory(): ?string
    {
        return $this->category;
    }

    public function setCategory(string $value): void
    {
        $this->category = trim(strtolower(Str::slug($value)));
    }

    public function getRelativePath(): ?string
    {
        return $this->relativePath;
    }

    public function setRelativePath(string $value): void
    {
        $this->relativePath = $value.DIRECTORY_SEPARATOR;
    }

    public function getRelativeHtmlPath(): ?string
    {
        return $this->relativeHtmlPath;
    }

    /**
     * Set the path to the code snippet file
     */
    public function setRelativeHtmlPath(): void
    {
        $this->relativeHtmlPath = $this->getRelativePath().'index.blade.php';
    }

    public function getRelativeThumbnailPath(): ?string
    {
        return $this->relativeThumbnailPath;
    }

    /**
     * Set the path to the preview file
     */
    public function setRelativeThumbnailPath(): void
    {
        $this->relativeThumbnailPath = $this->getRelativePath().'preview.png';
    }

    public function getAbsolutePath(): ?string
    {
        return $this->absolutePath;
    }

    public function setAbsolutePath(string $value): void
    {
        $this->absolutePath = $value.DIRECTORY_SEPARATOR;
    }

    public function getAbsoluteHtmlPath(): ?string
    {
        return $this->absoluteHtmlPath;
    }

    /**
     * Set the path to the code snippet file
     */
    public function setAbsoluteHtmlPath(): void
    {
        $this->absoluteHtmlPath = $this->getAbsolutePath().'index.blade.php';
    }

    public function getAbsoluteThumbnailPath(): ?string
    {
        return $this->absoluteThumbnailPath;
    }

    /**
     * Set the path to the preview file
     */
    public function setAbsoluteThumbnailPath(): void
    {
        $this->absoluteThumbnailPath = $this->getAbsolutePath().'preview.png';
    }

    public function getThumbnail(): ?string
    {
        // Get the thumbnail or if empty then use the placeholder image instead
        $thumbnail = $this->getAbsoluteThumbnailPath();
        if (! File::exists($thumbnail) || empty(File::get($thumbnail))) {
            $thumbnail = public_path('images/default_image.png');
        }

        // Get the preview file type, should be png
        $type = pathinfo($thumbnail, PATHINFO_EXTENSION);

        // Get the contents of the preview file
        $data = File::get($thumbnail);

        // Return the file contents as base64 encoding, so it displays as an image
        return 'data:image/'.$type.';base64,'.base64_encode($data);
    }

    public function setThumbnail($value): void
    {
        $this->thumbnail = $value;
    }

    public function getRawThumbnail(): ?string
    {
        // If the file exists
        if (File::exists($this->getAbsoluteThumbnailPath())) {
            return File::get($this->getAbsoluteThumbnailPath());
        }

        // Return a placeholder
        return File::get(public_path('images/default_image.png'));
    }

    /**
     * Get the contents of the snippet file.
     */
    public function getHtml(): ?string
    {
        $bladeFile = 'builder::frontend.'.$this->type.'.'.str_replace(DIRECTORY_SEPARATOR, '.',
                $this->getRelativeHtmlPath());
        $bladeFile = str_replace('.blade.php', '', $bladeFile);

        return view()->exists($bladeFile)
            ? view($bladeFile)->render()
            : null;
    }

    public function setHtml($value): void
    {
        $this->html = $value;
    }

    public function getRawHtml(): ?string
    {
        $bladeFile = 'builder::frontend.'.$this->type.'.'.str_replace(DIRECTORY_SEPARATOR, '.',
                $this->getRelativeHtmlPath());
        $bladeFile = str_replace('.blade.php', '', $bladeFile);

        return view()->exists($bladeFile)
            ? File::get(view($bladeFile)->getPath())
            : null;
    }

    /**
     * Has this item been extended and moved to the resources directory
     *
     * @return bool
     * @throws Exception
     */
    private function isExtended(): bool
    {
        return str_contains(
            $this->getResourcePath(),
            $this->getAbsolutePath()
        );
    }
}
