<?php
declare(strict_types=1);

namespace FlysystemOffload\Media;

use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use League\Flysystem\StorageAttributes;
use WP_Error;

class MediaHooks
{
    private const CUSTOM_IMAGE_EDITOR = 'FlysystemOffload\\Media\\ImageEditorImagick';

    private ?FilesystemOperator $filesystem = null;
    private bool $registered = false;
    private bool $metadataMirrorInProgress = false;

    public function setFilesystem(?FilesystemOperator $filesystem): void
    {
        $this->filesystem = $filesystem;

        if (class_exists(self::CUSTOM_IMAGE_EDITOR)) {
            \call_user_func([self::CUSTOM_IMAGE_EDITOR, 'bootWithFilesystem'], $filesystem);
        }
    }

    public function register(): void
    {
        if ($this->registered) {
            return;
        }

        add_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
        add_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10, 3);
        add_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10, 2);
        add_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10, 2);
        add_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10, 3);
        add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 10);

        $this->registered = true;
    }

    public function unregister(): void
    {
        if (! $this->registered) {
            return;
        }

        remove_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
        remove_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10);
        remove_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10);
        remove_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10);
        remove_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10);
        remove_action('delete_attachment', [$this, 'handleDeleteAttachment']);

        $this->registered = false;
    }

    public function filterImageEditors(array $editors): array
    {
        if (! class_exists(self::CUSTOM_IMAGE_EDITOR)) {
            return $editors;
        }

        $imagickIndex = array_search(\WP_Image_Editor_Imagick::class, $editors, true);

        if ($imagickIndex !== false) {
            unset($editors[$imagickIndex]);
        }

        array_unshift($editors, self::CUSTOM_IMAGE_EDITOR);

        return array_values(array_unique($editors));
    }

    public function handlePreMoveUploadedFile($override, array $file, string $destination)
    {
        if ($override !== null) {
            return $override;
        }

        if (! $this->isFlyPath($destination)) {
            return $override;
        }

        if (! $this->filesystem) {
            return new WP_Error(
                'flysystem_offload_missing_filesystem',
                __('No se pudo acceder al filesystem remoto.', 'flysystem-offload')
            );
        }

        $relativePath = $this->relativeFlyPath($destination);

        if ($relativePath === null) {
            return new WP_Error(
                'flysystem_offload_invalid_destination',
                __('Ruta de destino inválida para el stream fly://.', 'flysystem-offload')
            );
        }

        $directory = trim(dirname($relativePath), '.');

        try {
            if ($directory !== '') {
                $this->filesystem->createDirectory($directory);
            }

            $stream = @fopen($file['tmp_name'], 'rb');

            if (! $stream) {
                return new WP_Error(
                    'flysystem_offload_tmp_read_fail',
                    __('No se pudo leer el archivo temporal subido.', 'flysystem-offload')
                );
            }

            $this->filesystem->writeStream($relativePath, $stream);
        } catch (\Throwable $e) {
            if (isset($stream) && is_resource($stream)) {
                fclose($stream);
            }

            return new WP_Error(
                'flysystem_offload_write_fail',
                sprintf(
                    __('No se pudo guardar el archivo en el almacenamiento remoto: %s', 'flysystem-offload'),
                    $e->getMessage()
                )
            );
        }

        if (isset($stream) && is_resource($stream)) {
            fclose($stream);
        }

        @unlink($file['tmp_name']);

        return true;
    }

    public function filterReadImageMetadata($metadata, string $file)
    {
        if ($this->metadataMirrorInProgress || ! $this->isFlyPath($file)) {
            return $metadata;
        }

        $this->metadataMirrorInProgress = true;

        $temp = $this->mirrorToLocal($file);

        if (! is_wp_error($temp)) {
            $metadata = wp_read_image_metadata($temp);
            @unlink($temp);
        }

        $this->metadataMirrorInProgress = false;

        return $metadata;
    }

    public function filterGenerateAttachmentMetadata(array $metadata, int $attachmentId): array
    {
        if (isset($metadata['filesize'])) {
            return $metadata;
        }

        $file = get_attached_file($attachmentId);

        if ($file && file_exists($file)) {
            $metadata['filesize'] = filesize($file);
        }

        return $metadata;
    }

    public function filterUniqueFilenameFileList($files, string $dir, string $filename)
    {
        if (! $this->isFlyPath($dir) || ! $this->filesystem) {
            return $files;
        }

        $relativeDir = $this->relativeFlyPath($dir);

        if ($relativeDir === null) {
            return $files;
        }

        $existing = [];

        foreach ($this->filesystem->listContents($relativeDir, false) as $item) {
            /** @var StorageAttributes $item */
            if ($item->isDir()) {
                continue;
            }

            $existing[] = basename($item->path());
        }

        return $existing;
    }

    public function handleDeleteAttachment(int $attachmentId): void
    {
        $file = get_attached_file($attachmentId);

        if (! $file || ! $this->isFlyPath($file)) {
            return;
        }

        $meta = wp_get_attachment_metadata($attachmentId);

        if (! empty($meta['sizes'])) {
            foreach ($meta['sizes'] as $sizeInfo) {
                if (empty($sizeInfo['file'])) {
                    continue;
                }

                wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
            }
        }

        $original = get_post_meta($attachmentId, 'original_image', true);
        if ($original) {
            wp_delete_file(str_replace(basename($file), $original, $file));
        }

        $backup = get_post_meta($attachmentId, '_wp_attachment_backup_sizes', true);
        if (is_array($backup)) {
            foreach ($backup as $sizeInfo) {
                if (empty($sizeInfo['file'])) {
                    continue;
                }

                wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
            }
        }

        wp_delete_file($file);
    }

    protected function mirrorToLocal(string $remotePath)
    {
        if (! $this->filesystem || ! $this->isFlyPath($remotePath)) {
            return $this->mirrorViaNativeCopy($remotePath);
        }

        if (! function_exists('wp_tempnam')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        $temp = wp_tempnam(wp_basename($remotePath));

        if (! $temp) {
            return new WP_Error(
                'flysystem_offload_temp_fail',
                __('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload')
            );
        }

        $relative = $this->relativeFlyPath($remotePath);

        if ($relative === null) {
            @unlink($temp);

            return new WP_Error(
                'flysystem_offload_remote_copy_fail',
                __('No se pudo determinar la ruta remota del archivo.', 'flysystem-offload')
            );
        }

        try {
            $source = $this->filesystem->readStream($relative);

            if (! is_resource($source)) {
                throw new \RuntimeException('No se pudo abrir el stream remoto.');
            }

            $target = fopen($temp, 'wb');

            if (! $target) {
                throw new \RuntimeException('No se pudo abrir el archivo temporal en disco.');
            }

            stream_copy_to_stream($source, $target);
        } catch (\Throwable $e) {
            if (isset($source) && is_resource($source)) {
                fclose($source);
            }
            if (isset($target) && is_resource($target)) {
                fclose($target);
            }

            @unlink($temp);

            return new WP_Error(
                'flysystem_offload_remote_copy_fail',
                sprintf(
                    __('No se pudo copiar la imagen desde el almacenamiento remoto: %s', 'flysystem-offload'),
                    $e->getMessage()
                )
            );
        }

        if (isset($source) && is_resource($source)) {
            fclose($source);
        }
        if (isset($target) && is_resource($target)) {
            fclose($target);
        }

        return $temp;
    }

    private function mirrorViaNativeCopy(string $remotePath)
    {
        if (! function_exists('wp_tempnam')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        $temp = wp_tempnam(wp_basename($remotePath));

        if (! $temp) {
            return new WP_Error(
                'flysystem_offload_temp_fail',
                __('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload')
            );
        }

        if (@copy($remotePath, $temp)) {
            return $temp;
        }

        @unlink($temp);

        return new WP_Error(
            'flysystem_offload_remote_copy_fail',
            __('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
        );
    }

    protected function isFlyPath(string $path): bool
    {
        return strncmp($path, 'fly://', 6) === 0;
    }

    protected function relativeFlyPath(string $path): ?string
    {
        if (! $this->isFlyPath($path)) {
            return null;
        }

        return ltrim(substr($path, 6), '/');
    }
}
