<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Gallery;
use App\Models\Image;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

class SparkboothUploadController extends Controller
{
    private const ALLOWED_MIMES = [
        'image/jpeg',
        'image/png',
        'image/gif',
        'image/bmp',
        'image/webp',
    ];

    private const MAX_FILE_SIZE = 10240 * 1024; // 10 MB

    public function store(Request $request)
    {
        $this->validateRequest($request);

        [$gallery, $authMethod] = $this->resolveGallery($request);

        if (! $gallery) {
            return $this->respondError($request, 'Invalid credentials.', Response::HTTP_FORBIDDEN);
        }

        if (! $gallery->upload_enabled) {
            return $this->respondError($request, 'Uploads are disabled for this gallery.', Response::HTTP_FORBIDDEN, $gallery);
        }

        if ($authMethod === 'token'
            && $gallery->upload_token_expires_at
            && now()->greaterThanOrEqualTo($gallery->upload_token_expires_at)
        ) {
            return $this->respondError($request, 'Upload token expired.', Response::HTTP_FORBIDDEN, $gallery);
        }

        [$file, $base64Payload] = $this->extractMediaPayload($request);

        if (! $file && $base64Payload === null) {
            return $this->respondError($request, 'No media payload provided.', Response::HTTP_BAD_REQUEST, $gallery);
        }

        try {
            [$relativePath, $publicUrl] = $this->persistMedia(
                $gallery,
                $file,
                $base64Payload,
                $request->input('filename')
            );
        } catch (RuntimeException $exception) {
            return $this->respondError($request, $exception->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY, $gallery);
        }

        Image::create([
            'gallery_id' => $gallery->id,
            'path' => $relativePath,
            'is_public' => true,
        ]);

        return $this->respondSuccess($request, $publicUrl, $gallery);
    }

    private function validateRequest(Request $request): void
    {
        $request->validate([
            'token' => ['nullable', 'string'],
            'username' => ['nullable', 'string'],
            'password' => ['nullable', 'string'],
            'media' => ['nullable'],
            'file' => ['nullable', 'file', 'mimes:jpeg,png,gif,bmp,webp', 'max:10240'],
            'filename' => ['nullable', 'string'],
            'name' => ['nullable', 'string'],
            'email' => ['nullable', 'string'],
            'message' => ['nullable', 'string'],
            'response_format' => ['nullable', 'in:json,xml'],
        ]);
    }

    /**
     * @return array{0: ?Gallery, 1: ?string}
     */
    private function resolveGallery(Request $request): array
    {
        if ($request->filled('token')) {
            return [$this->resolveGalleryByToken($request->string('token')), 'token'];
        }

        if ($request->filled('username')) {
            return [
                $this->resolveGalleryByCredentials(
                    $request->string('username'),
                    $request->string('password')
                ),
                'credentials',
            ];
        }

        return [null, null];
    }

    private function resolveGalleryByToken(string $token): ?Gallery
    {
        $galleries = Gallery::query()
            ->whereNotNull('upload_token_hash')
            ->where('upload_enabled', true)
            ->get();

        foreach ($galleries as $gallery) {
            if ($gallery->upload_token_hash && Hash::check($token, $gallery->upload_token_hash)) {
                return $gallery;
            }
        }

        return null;
    }

    private function resolveGalleryByCredentials(?string $username, ?string $password): ?Gallery
    {
        if (blank($username) || blank($password)) {
            return null;
        }

        $normalized = Str::of($username)->lower()->trim()->value();

        $gallery = Gallery::query()
            ->whereNotNull('sparkbooth_username')
            ->where('sparkbooth_username', $normalized)
            ->first();

        if (! $gallery) {
            return null;
        }

        if (! hash_equals((string) $gallery->sparkbooth_password, (string) $password)) {
            return null;
        }

        return $gallery;
    }

    /**
     * @return array{0: ?UploadedFile, 1: ?string}
     */
    private function extractMediaPayload(Request $request): array
    {
        $file = $request->file('media') ?? $request->file('file');

        if ($file) {
            return [$file, null];
        }

        $payload = $request->input('media');

        if ($payload === null || $payload === '') {
            return [null, null];
        }

        return [null, $payload];
    }

    /**
     * @return array{0: string, 1: string}
     */
    private function persistMedia(
        Gallery $gallery,
        ?UploadedFile $file,
        ?string $base64Payload,
        ?string $preferredFilename
    ): array {
        $directory = trim($gallery->images_path, '/');

        if ($directory === '') {
            $directory = 'uploads';
        }

        if ($file) {
            $extension = $file->getClientOriginalExtension();
            $filename = $this->buildFilename($extension, $preferredFilename);
            $relativePath = $directory.'/'.$filename;

            $file->storeAs($directory, $filename, 'public');

            return [$relativePath, asset('storage/'.$relativePath)];
        }

        if ($base64Payload === null) {
            throw new RuntimeException('No media payload provided.');
        }

        [$binary, $extension] = $this->decodeBase64Media($base64Payload);
        $filename = $this->buildFilename($extension, $preferredFilename);
        $relativePath = $directory.'/'.$filename;

        if (! Storage::disk('public')->put($relativePath, $binary)) {
            throw new RuntimeException('Failed to store uploaded image.');
        }

        return [$relativePath, asset('storage/'.$relativePath)];
    }

    /**
     * @return array{0: string, 1: string}
     */
    private function decodeBase64Media(string $payload): array
    {
        $mime = null;

        if (preg_match('/^data:(image\\/[^;]+);base64,(.+)$/', $payload, $matches)) {
            $mime = $matches[1];
            $payload = $matches[2];
        }

        $binary = base64_decode($payload, true);

        if ($binary === false) {
            throw new RuntimeException('Invalid media payload.');
        }

        if (strlen($binary) > self::MAX_FILE_SIZE) {
            throw new RuntimeException('File too large.');
        }

        if (! $mime) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $mime = $finfo ? finfo_buffer($finfo, $binary) : null;

            if ($finfo) {
                finfo_close($finfo);
            }
        }

        if (! $mime || ! $this->isAllowedMime($mime)) {
            throw new RuntimeException('Unsupported image type.');
        }

        $extension = $this->extensionFromMime($mime) ?? 'jpg';

        return [$binary, $extension];
    }

    private function isAllowedMime(string $mime): bool
    {
        return in_array(strtolower($mime), self::ALLOWED_MIMES, true);
    }

    private function extensionFromMime(string $mime): ?string
    {
        return match (strtolower($mime)) {
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'image/gif' => 'gif',
            'image/bmp' => 'bmp',
            'image/webp' => 'webp',
            default => null,
        };
    }

    private function buildFilename(?string $extension, ?string $preferred = null): string
    {
        $extension = strtolower($extension ?: 'jpg');
        $base = $preferred
            ? Str::slug(pathinfo($preferred, PATHINFO_FILENAME))
            : 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6);

        if ($base === '') {
            $base = 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6);
        }

        return $base.'.'.$extension;
    }

    private function respondSuccess(Request $request, string $url, ?Gallery $gallery = null)
    {
        $format = $this->determineResponseFormat($request, $gallery);

        if ($format === 'xml') {
            $body = $this->buildXmlResponse('ok', $url);

            return response($body, Response::HTTP_OK)->header('Content-Type', 'application/xml');
        }

        return response()->json([
            'status' => true,
            'error' => null,
            'url' => $url,
        ]);
    }

    private function respondError(Request $request, string $message, int $status, ?Gallery $gallery = null)
    {
        $format = $this->determineResponseFormat($request, $gallery);

        if ($format === 'xml') {
            $body = $this->buildXmlResponse('fail', null, $message);

            return response($body, $status)->header('Content-Type', 'application/xml');
        }

        return response()->json([
            'status' => false,
            'error' => $message,
            'url' => null,
        ], $status);
    }

    private function determineResponseFormat(Request $request, ?Gallery $gallery = null): string
    {
        $candidate = Str::of((string) $request->input('response_format'))->lower()->value();

        if (in_array($candidate, ['json', 'xml'], true)) {
            return $candidate;
        }

        $accept = Str::of((string) $request->header('Accept'))->lower()->value();

        if (str_contains($accept, 'application/xml') || str_contains($accept, 'text/xml')) {
            return 'xml';
        }

        if ($gallery && in_array($gallery->sparkbooth_response_format, ['json', 'xml'], true)) {
            return $gallery->sparkbooth_response_format;
        }

        return 'json';
    }

    private function buildXmlResponse(string $status, ?string $url = null, ?string $errorMessage = null): string
    {
        if ($status === 'ok') {
            $urlAttr = $url ? ' url="'.e($url).'"' : '';

            return '<?xml version="1.0" encoding="UTF-8"?><rsp status="ok"'.$urlAttr.' />';
        }

        $errorAttr = $errorMessage ? '<err msg="'.e($errorMessage).'" />' : '';

        return '<?xml version="1.0" encoding="UTF-8"?><rsp status="fail">'.$errorAttr.'</rsp>';
    }
}
