app/Http/Controllers/Api/AboutController.php
<?php

namespace App\Http\Controllers\Api;

use App\Console\Commands\RotateScreenshots;
use App\Helpers\ModuleHelper;
use App\Helpers\ReportHelper;
use App\Helpers\StorageCleaner;
use App\Helpers\Version;
use Artisan;
use Cache;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use JsonException;
use Settings;

class AboutController extends Controller
{
    private Client $client;

    public function __construct()
    {
        $this->client = new Client([
            'base_uri' => config('app.stats_collector_url') . '/v2/',
            'headers' => ['x-cattr-instance' => Settings::scope('core')->get('instance')],
        ]);
    }

    /**
     * @api             {get} /about/reports Get Available Report Formats
     * @apiDescription  Retrieves the list of available report formats and their corresponding MIME types.
     *
     * @apiVersion      4.0.0
     * @apiName         GetReportFormats
     * @apiGroup        About
     *
     * @apiSuccess {Object}     types                        List of available report formats and their MIME types.
     * @apiSuccess {String}     types.csv                    MIME type for CSV format.
     * @apiSuccess {String}     types.xlsx                   MIME type for XLSX format.
     * @apiSuccess {String}     types.pdf                    MIME type for PDF format.
     * @apiSuccess {String}     types.xls                    MIME type for XLS format.
     * @apiSuccess {String}     types.ods                    MIME type for ODS format.
     * @apiSuccess {String}     types.html                   MIME type for HTML format.
     *
     * @apiSuccessExample {json} Success Response:
     *  HTTP/1.1 200 OK
     *  {
     *      "types": {
     *        "csv": "text/csv",
     *        "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
     *        "pdf": "application/pdf",
     *        "xls": "application/vnd.ms-excel",
     *        "ods": "application/vnd.oasis.opendocument.spreadsheet",
     *        "html": "text/html"
     *      }
     *  }
     *
      * @apiUse         400Error
     * @apiUse         ValidationError
     * @apiUse         UnauthorizedError
     * @apiUse         ForbiddenError
     */

        public function reports(): JsonResponse
        {
            return responder()->success([
                'types' => ReportHelper::getAvailableReportFormats(),
            ])->respond();
        }

    /**
     * Returns information about this instance.
     * @throws JsonException
     */
    /**
     * @api             {get} /about Get Application and Module Information
     * @apiDescription  Retrieves information about the application instance, modules, and image version details.
     *
     * @apiVersion      4.0.0
     * @apiName         GetAppInfo
     * @apiGroup        About
     *
     * @apiSuccess {Object}     app                          Application information.
     * @apiSuccess {String}     app.version                  The current version of the application.
     * @apiSuccess {String}     app.instance_id              The unique identifier of the application instance.
     * @apiSuccess {Boolean}    app.vulnerable               Indicates if the application is vulnerable.
     * @apiSuccess {String}     app.last_version             The latest version available for the application.
     * @apiSuccess {String}     app.message                  Any important message related to the application.
     *
     * @apiSuccess {Object[]}   modules                      List of modules integrated into the application.
     * @apiSuccess {String}     modules.name                 Name of the module.
     * @apiSuccess {String}     modules.version              Current version of the module.
     * @apiSuccess {Boolean}    modules.enabled              Indicates if the module is enabled.
     *
     * @apiSuccess {Object}     image                        Information about the image version.
     * @apiSuccess {String}     image.version                The version of the image (if available).
     * @apiSuccess {Boolean}    image.vulnerable             Indicates if the image is vulnerable.
     * @apiSuccess {String}     image.last_version           The latest available version for the image.
     * @apiSuccess {String}     image.message                Any important message related to the image.
     *
     * @apiSuccessExample {json} Success Response:
     *  HTTP/1.1 200 OK
     *  {
     *      "app": {
     *        "version": "dev",
     *        "instance_id": null,
     *        "vulnerable": null,
     *        "last_version": null,
     *        "message": null
     *      },
     *      "modules": [
     *        {
     *          "name": "JiraIntegration",
     *          "version": "3.0.0",
     *          "enabled": false
     *        }
     *      ],
     *        "image": {
     *        "version": null,
     *        "vulnerable": null,
     *        "last_version": null,
     *        "message": null
     *     }
     *  }
     *
     * @apiUse         400Error
     * @apiUse         ValidationError
     * @apiUse         UnauthorizedError
     * @apiUse         ForbiddenError
     */

    public function __invoke(): JsonResponse
    {
        $imageVersion = getenv('IMAGE_VERSION', true) ?: null;

        $releaseInfo = $this->requestReleaseInfo();
        $modulesInfo = $this->requestModulesInfo();
        $imageInfo = ($imageVersion) ? $this->requestImageInfo($imageVersion) : false;

        return responder()->success([
            'app' => [
                'version' => config('app.version'),
                'instance_id' => Settings::scope('core')->get('instance'),
                'vulnerable' => optional($releaseInfo)->vulnerable,
                'last_version' => optional($releaseInfo)->lastVersion,
                'message' => optional($releaseInfo)->flashMessage,
            ],
            'modules' => $modulesInfo,
            'image' => [
                'version' => $imageVersion,
                'vulnerable' => optional($imageInfo)->vulnerable,
                'last_version' => optional($imageInfo)->lastVersion,
                'message' => optional($imageInfo)->flashMessage,
            ]
        ])->respond();
    }

    private function requestReleaseInfo(): ?object
    {
        try {
            return json_decode(
                $this->client->get('release/' . config('app.version'))->getBody()->getContents(),
                false,
                512,
                JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
            );
        } catch (Exception) {
            return null;
        }
    }

    /**
     * @throws JsonException
     */
    private function requestModulesInfo(): array
    {
        $options = [
            'json' => ModuleHelper::getModulesInfo(),
        ];

        try {
            return array_map(static function ($el) {
                $el['version'] = (string)(new Version($el['name']));

                return $el;
            }, json_decode(
                $this->client->post('modules', $options)->getBody()->getContents(),
                true,
                512,
                JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
            )['modules']);
        } catch (Exception) {
            return ModuleHelper::getModulesInfo();
        }
    }

    private function requestImageInfo(string $imageVersion): ?object
    {
        try {
            return json_decode(
                $this->client->get("image/$imageVersion")->getBody()->getContents(),
                false,
                512,
                JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
            );
        } catch (Exception) {
            return null;
        }
    }

    /**
     * @throws BindingResolutionException
     */
    /**
     * @api             {get} /storage Get Storage Information
     * @apiDescription  Retrieves information about the storage space, thinning status, and available screenshots.
     *
     * @apiVersion      4.0.0
     * @apiName         GetStorageInfo
     * @apiGroup        Storage
     *
     * @apiSuccess {Object}     space                          Information about the storage space.
     * @apiSuccess {Number}     space.left                     The amount of free space left (in bytes).
     * @apiSuccess {Number}     space.used                     The amount of space currently used (in bytes).
     * @apiSuccess {Number}     space.total                    The total amount of storage space available (in bytes).
     *
     * @apiSuccess {Number}     threshold                      The storage usage threshold percentage before thinning is needed.
     * @apiSuccess {Boolean}    need_thinning                  Indicates if the storage requires thinning.
     * @apiSuccess {Number}     screenshots_available          The number of available screenshots in the storage.
     *
     * @apiSuccess {Object}     thinning                       Information about the thinning process.
     * @apiSuccess {Boolean}    thinning.now                   Indicates if the thinning process is currently ongoing.
     * @apiSuccess {String}     thinning.last                  Timestamp of the last thinning process.
     *
     * @apiSuccessExample {json} Success Response:
     *  HTTP/1.1 200 OK
     *  {
     *      "space": {
     *        "left": 26579533824,
     *        "used": 178705711104,
     *        "total": 205285244928
     *      },
     *      "threshold": 75,
     *      "need_thinning": true,
     *      "screenshots_available": 0,
     *      "thinning": {
     *        "now": null,
     *        "last": null
     *      }
     *  }
     *
     * @apiUse         400Error
     * @apiUse         ValidationError
     * @apiUse         UnauthorizedError
     * @apiUse         ForbiddenError
     */

    public function storage(): JsonResponse
    {
        return responder()->success([
            'space' => [
                'left' => StorageCleaner::getFreeSpace(),
                'used' => StorageCleaner::getUsedSpace(),
                'total' => config('cleaner.total_space'),
            ],
            'threshold' => config('cleaner.threshold'),
            'need_thinning' => StorageCleaner::needThinning(),
            'screenshots_available' => StorageCleaner::countAvailableScreenshots(),
            'thinning' => [
                'now' => Cache::store('octane')->get('thinning_now'),
                'last' => Cache::store('octane')->get('last_thin'),
            ]
        ])->respond();
    }
   /**
    * @api             {get} /storage Get Storage Information
    * @apiDescription  Retrieves information about the storage space, thinning status, and available screenshots.
    *
    * @apiVersion      4.0.0
    * @apiName         GetStorageInfo
    * @apiGroup        Storage
    *
    * @apiSuccessExample {json} Response Example
    *  HTTP/1.1 204 No Content
    *  {
    *  }
    *
    * @apiUse          400Error
    * @apiUse          UnauthorizedError
    *
    */
    public function startStorageClean(): JsonResponse
    {
        Artisan::queue(RotateScreenshots::class);

        return responder()->success()->respond(204);
    }
}
