<?php

namespace App\Http\Requests;

use App\Http\Traits\ConvertsBase64ToFiles;
use App\Models\SnipeModel;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Intervention\Image\Exception\NotReadableException;
use Intervention\Image\Facades\Image;

class ImageUploadRequest extends Request
{
    use ConvertsBase64ToFiles;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {

        return [
            'image' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif',
            'avatar' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif',
            'favicon' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,image/x-icon,image/vnd.microsoft.icon,ico',
        ];
    }

    public function response(array $errors)
    {
        return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
    }

    /**
     * Fields that should be traited from base64 to files
     */
    protected function base64FileKeys(): array
    {
        /**
         * image_source is here just legacy reasons. Api\AssetController
         * had it once to allow encoded image uploads.
         */
        return [
            'avatar' => 'auto',
            'image' => 'auto',
            'image_source' => 'auto',
        ];
    }

    /**
     * Handle and store any images attached to request
     *
     * @param  SnipeModel  $item  Item the image is associated with
     * @param  string  $path  location for uploaded images, defaults to uploads/plural of item type.
     * @return SnipeModel Target asset is being checked out to.
     */
    public function handleImages($item, $w = 600, $form_fieldname = 'image', $path = null, $db_fieldname = 'image')
    {

        $type = class_basename(get_class($item));

        if (is_null($path)) {

            $path = strtolower(str_plural($type));

            if ($type == 'AssetModel') {
                $path = 'models';
            }

            if ($type == 'user') {
                $path = 'avatars';
            }

        }

        // SettingsController passes '' to mean "disk root".
        // Normalize and use a single prefix so we never have a leading-slash
        // key (S3 stores `/foo.png` and `foo.png` as distinct objects) and
        // never HEAD an empty key (which S3's HeadObject validator rejects
        // outright — the bug behind #18267).
        $path = trim((string) $path, '/');
        $prefix = $path === '' ? '' : $path.'/';

        if ($path !== '' && ! Storage::disk('public')->exists($path)) {
            Storage::disk('public')->makeDirectory($path);
        }

        if ($this->offsetGet($form_fieldname) instanceof UploadedFile) {
            $image = $this->offsetGet($form_fieldname);
        } elseif ($this->hasFile($form_fieldname)) {
            $image = $this->file($form_fieldname);
        }

        if ((isset($image)) && ($image != '')) {

            $ext = $image->guessExtension();
            $file_name = $type.'-'.$form_fieldname.($item->id ?? '-'.$item->id).'-'.str_random(10).'.'.$ext;

            // Track whether the new file actually landed on disk. Storage::put
            // can return false without throwing (disks default to non-throwing
            // mode). Before, the ordering was put -> deleteExistingImage ->
            // reassign, all unconditional, so a failed put still destroyed
            // the current image and left the model referencing a file that
            // was never written. Keep the old image intact unless we confirm
            // the new one is there.
            $wroteNewFile = false;

            if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
                // If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
                // icons or avif, and webp support and needs to be compiled into gd for resizing to be available
                $wroteNewFile = (bool) Storage::disk('public')->put($prefix.$file_name, file_get_contents($image));

            } elseif ($image->getMimeType() == 'image/svg+xml') {
                // If the file is an SVG, we need to clean it and NOT encode it
                $sanitizer = new Sanitizer;
                $dirtySVG = file_get_contents($image->getRealPath());
                $cleanSVG = $sanitizer->sanitize($dirtySVG);

                try {
                    $wroteNewFile = (bool) Storage::disk('public')->put($prefix.$file_name, $cleanSVG);
                } catch (\Exception $e) {
                    Log::debug($e);
                }
            } else {

                try {
                    $upload = Image::make($image->getRealPath())->setFileInfoFromPath($image->getRealPath())->resize(null, $w, function ($constraint) {
                        $constraint->aspectRatio();
                        $constraint->upsize();
                    })->orientate();

                } catch (NotReadableException $e) {
                    Log::debug($e);
                    $validator = Validator::make([], []);
                    $validator->errors()->add($form_fieldname, trans('general.unaccepted_image_type', ['mimetype' => $image->getClientMimeType()]));

                    throw new ValidationException($validator);
                }

                // This requires a string instead of an object, so we use ($string)
                $wroteNewFile = (bool) Storage::disk('public')->put($prefix.$file_name, (string) $upload->encode());

            }

            if ($wroteNewFile) {
                // Only touch the existing image and the model reference AFTER
                // confirming the new file is on disk. deleteExistingImage
                // itself now also refuses to null the model when the delete
                // fails, so a partial cleanup does not leave the model
                // pointing at a phantom file either way.
                $item = $this->deleteExistingImage($item, $path, $db_fieldname);
                $item->{$db_fieldname} = $file_name;
            } else {
                Log::warning('Image upload failed to write to disk; keeping existing image reference intact.', [
                    'item_type' => $type,
                    'item_id' => $item->id ?? null,
                    'target_path' => $prefix.$file_name,
                ]);
            }

            // If the user isn't uploading anything new but wants to delete their old image, do so
        } elseif ($this->input('image_delete') == '1') {
            $item = $this->deleteExistingImage($item, $path, $db_fieldname);
        }

        return $item;
    }

    public function deleteExistingImage($item, $path = null, $db_fieldname = 'image')
    {

        if ($item->{$db_fieldname} != '') {
            try {
                // Same path normalization as handleImages. Branding callers
                // pass '' for the disk root, and we don't want to produce a
                // leading-slash key on S3.
                $path = trim((string) $path, '/');
                $key = $path === '' ? $item->{$db_fieldname} : $path.'/'.$item->{$db_fieldname};
                $deleted = Storage::disk('public')->delete($key);

                // Only null the model reference if the delete actually
                // succeeded. Before, the field was cleared unconditionally
                // even when Storage::delete returned false (silent-fail
                // mode on the default local disk). The result was a model
                // row that reported "no image" while the file remained on
                // disk, orphaned.
                if ($deleted) {
                    $item->{$db_fieldname} = null;
                } else {
                    Log::warning('Storage delete returned false; keeping model reference so operators can retry.', [
                        'item_type' => class_basename(get_class($item)),
                        'item_id' => $item->id ?? null,
                        'key' => $key,
                    ]);
                }
            } catch (\Exception $e) {
                Log::debug($e);
            }
        }

        return $item;
    }
}
