<?php

// SPDX-FileCopyrightText: 2025 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Notifications\Api\V1;

use Exception;
use Generator;
use Icinga\Exception\Http\HttpBadRequestException;
use Icinga\Exception\Json\JsonDecodeException;
use Icinga\Exception\Json\JsonEncodeException;
use Icinga\Module\Notifications\Api\ApiCore;
use Icinga\Module\Notifications\Api\Exception\InvalidFilterParameterException;
use Icinga\Module\Notifications\Common\Database;
use Icinga\Module\Notifications\Common\HttpMethod;
use Icinga\Util\Json;
use ipl\Sql\Compat\FilterProcessor;
use ipl\Sql\Select;
use ipl\Stdlib\Filter\Condition;
use ipl\Web\Filter\QueryString;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Ramsey\Uuid\Uuid;
use stdClass;

/**
 * Base class for API version 1.
 *
 * This class provides common functionality for API endpoints in version 1 of the Icinga Notifications API.
 */
#[OA\OpenApi(
    info: new OA\Info(
        version: '1.0.0',
        description: 'API for managing notification Channels, Contacts, and Contact Groups in Icinga.',
        title: 'Icinga Notifications API',
    ),
    servers: [
        new OA\Server(
            url: '/icingaweb2/notifications/api/v1',
            description: 'Local server',
        )
    ],
    security: [
        ['BasicAuth' => []],
    ],
)]
#[OA\Tag(
    name: 'Contacts',
    description: 'Operations related to notification Contacts'
)]
#[OA\Tag(
    name: 'Contact Groups',
    description: 'Operations related to notification Contact Groups'
)]
#[OA\Tag(
    name: 'Channels',
    description: 'Operations related to notification Channels'
)]
#[OA\SecurityScheme(
    securityScheme: 'BasicAuth',
    type: 'http',
    description: 'Basic authentication for API access',
    scheme: 'basic',
)]
abstract class ApiV1 extends ApiCore
{
    /**
     * This constant defines the version of the API.
     *
     * @var string
     */
    public const VERSION = 'v1';

    /**
     * @throws HttpBadRequestException If the request is not valid.
     */
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $identifier = $request->getAttribute('identifier');
        $queryFilter = $request->getUri()->getQuery();

        return match ($request->getAttribute('httpMethod')) {
            HttpMethod::PUT    => $this->put($identifier, $this->getValidRequestBody($request)),
            HttpMethod::POST   => $this->post($identifier, $this->getValidRequestBody($request)),
            HttpMethod::GET    => $this->get($identifier, $queryFilter),
            HttpMethod::DELETE => $this->delete($identifier),
        };
    }

    /**
     * Override this method to modify the row before it is returned in the response.
     *
     * @param stdClass $row
     *
     * @return void
     */
    public function prepareRow(stdClass $row): void
    {
    }

    /**
     * Create a filter from the filter string.
     *
     * @param string $queryFilter
     * @param array $allowedColumns
     * @param string $idColumnName
     *
     * @return array|bool Returns an array of filter rules or false if no filter string is provided.
     *
     * @throws HttpBadRequestException If the filter string cannot be parsed.
     */
    protected function assembleFilter(string $queryFilter, array $allowedColumns, string $idColumnName): array|bool
    {
        if (empty($queryFilter)) {
            return false;
        }

        try {
            $filterRule = QueryString::fromString($queryFilter)
                ->on(
                    QueryString::ON_CONDITION,
                    function (Condition $condition) use ($allowedColumns, $idColumnName) {
                        $column = $condition->getColumn();
                        if (! in_array($column, $allowedColumns)) {
                            throw new InvalidFilterParameterException($column);
                        }

                        if ($column === 'id') {
                            if (! Uuid::isValid($condition->getValue())) {
                                throw new HttpBadRequestException('The given filter id is not a valid UUID');
                            }

                            $condition->setColumn($idColumnName);
                        }
                    }
                )->parse();

            return FilterProcessor::assembleFilter($filterRule);
        } catch (Exception $e) {
            if ($e instanceof InvalidFilterParameterException) {
                throw $e;
            }

            throw new HttpBadRequestException($e->getMessage());
        }
    }

    /**
     * Validate that the request has a JSON content type and return the parsed JSON content.
     *
     * @param ServerRequestInterface $request The request-object to validate.
     *
     * @return array The validated JSON content as an associative array.
     *
     * @throws HttpBadRequestException If the content type is not application/json.
     */
    private function getValidRequestBody(ServerRequestInterface $request): array
    {
        if ($request->getHeaderLine('Content-Type') !== 'application/json') {
            throw new HttpBadRequestException('Invalid request header: Content-Type must be application/json');
        }

        if (! empty($parsedBody = $request->getParsedBody()) && is_array($parsedBody)) {
            return $parsedBody;
        }

        $msgPrefix = 'Invalid request body: ';
        $body = $request->getBody()->getContents();

        if (empty($body)) {
            throw new HttpBadRequestException($msgPrefix . 'given content is empty');
        }

        try {
            $validBody = Json::decode($body, true);
        } catch (JsonDecodeException) {
            throw new HttpBadRequestException($msgPrefix . 'given content is not a valid JSON');
        }

        return $validBody;
    }

    /**
     * Generates a streamable response for large datasets.
     *
     * Enables efficient delivery of data by yielding results in batches.
     *
     * @param Select $stmt The SQL select statement to execute.
     * @param int $batchSize The number of rows to fetch in each batch (default is 500).
     *
     * @return Generator Yields JSON-encoded strings representing the content.
     *
     * @throws JsonEncodeException
     */
    protected function createContentGenerator(
        Select $stmt,
        int $batchSize = 500
    ): Generator {
        $stmt->limit($batchSize);
        $offset = 0;

        if ($stmt->getOrderBy() === null) {
            $stmt->orderBy('id');
        }

        yield '{"data":[';
        $res = Database::get()->select($stmt->offset($offset));
        do {
            /** @var stdClass $row */
            foreach ($res as $i => $row) {
                $this->prepareRow($row);

                if ($i > 0 || $offset !== 0) {
                    yield ",";
                }

                yield Json::sanitize($row);
            }

            $offset += $batchSize;
            $res = Database::get()->select($stmt->offset($offset));
        } while ($res->rowCount());

        yield ']}';
    }
}
