<?php declare(strict_types=1);

namespace App\Controller;

error_reporting(error_level: E_ALL);

use App\Entity\Domain;
use App\Entity\DynDNS;
use App\Repository\ApikeyRepository;
use App\Repository\DomainRepository;
use App\Repository\DynDNSRepository;
use App\Repository\PanelRepository;
use Monolog\Logger;
use OpenApi\Attributes as OAT;
use UnhandledMatchError;


// TODO attributes for swaggerUI

/**
 *
 */
#[OAT\Info(version: '0.0.1', title: 'bindAPI')]
#[OAT\Server(
    url: "{schema}://{hostname}/api",
    description: "The bindAPI URL.",
    variables: [
        new OAT\ServerVariable(
            serverVariable: "schema",
            default: "https",
            enum: ["https", "http"]
        ),
        new OAT\ServerVariable(
            serverVariable: "hostname",
            default: "ns2.24unix.net",
        )
    ]
)]
#[OAT\Tag(
    name: "Server"
)]
#[OAT\SecurityScheme(
    securityScheme: "Authorization",
    type: "apiKey",
    description: "description",
    name: "X-API-Key",
    in: "header"
)]
class RequestController
{
    private string $status;
    private string $response;
    private string $message;
    private array $result;
    private string $requestMethod;
    private array $uri;


    /**
     * @param ApiController $apiController
     * @param ApikeyRepository $apikeyRepository
     * @param DomainController $domainController
     * @param DomainRepository $domainRepository
     * @param DynDNSRepository $dynDNSRepository
     * @param PanelRepository $panelRepository
     * @param ConfigController $configController
     * @param EncryptionController $encryptionController
     * @param Logger $logger
     */
    public function __construct(
        private readonly ApiController        $apiController,
        private readonly ApikeyRepository     $apikeyRepository,
        private readonly DomainController     $domainController,
        private readonly DomainRepository     $domainRepository,
        private readonly DynDNSRepository     $dynDNSRepository,
        private readonly PanelRepository      $panelRepository,
        private readonly ConfigController     $configController,
        private readonly EncryptionController $encryptionController,
        private readonly Logger               $logger)
    {
        $this->status = '';
        $this->response = '';
        $this->message = '';
        $this->result = [];
    }

    /**
     * @return void
     */
    #[OAT\Get(
        path: '/domains',
        operationId: 'getAllDomains',
        description: 'Returns a list of all domains on this server.',
        summary: 'Listing all domains.',
//		security: [
//			'Authorization' => [
//
//				"read:api"
//			]
//		],
        servers: [],
        tags: ['Domains'],
        responses: [
            new OAT\Response(
                response: 200,
                description: 'OK'
            ),
            new OAT\Response(
                response: 401,
                description: 'API key is missing or invalid.'
            ),
            new OAT\Response(
                response: 404,
                description: 'Domain not found.'
            )]
    )]
    private function handleAllDomainsGetRequest(): void
    {
        $domains = $this->domainRepository->findAll();
        $resultDomain = [];
        foreach ($domains as $singleDomain) {
            $domain = [
                'id' => $singleDomain->getId(),
                'name' => $singleDomain->getName(),
                'panel' => $singleDomain->getPanel()
            ];
            $resultDomain[] = $domain;
        }
        $this->result = $resultDomain;
    }

    /**
     */
    private function handlePing(): void
    {
        if ($this->checkPassword()) {
            $this->status = '200 OK';
            $this->response = 'pong';
        } else {
            $this->status = '401 Unauthorized';
            $this->message = 'API key is missing or invalid';
        }
    }


    /**
     * @return void
     */
    private function handleDomains(): void
    {
        if ($this->checkPassword()) {
            try {
                match ($this->requestMethod) {
                    'GET' => $this->handleDomainsGetRequest(),
                    'POST' => $this->handleDomainsPostRequest(),
                    'PUT' => $this->handleDomainsPutRequest(),
                    'DELETE' => $this->handleDomainsDeleteRequest()
                };
            } catch (UnhandledMatchError) {
                $this->status = '400 Bad Request';
                $this->message = "unknown request method: $this->requestMethod";
            }
        }
    }


    /**
     * @OA\Tag(name = "Server")
     * @OA\Get(
     *     path = "/ping",
     *     summary = "Returning pong.",
     *     description = "Can be used to check API or server availability.",
     *     tags={"Server"},
     *     @OA\Response(response = "200", description = "OK"),
     *     @OA\Response(response = "401", description = "API key is missing or invalid."),
     *     security={
     *         {"Authorization":{"read"}}
     *     }
     * )
     *
     * @OA\Tag(name = "Domains")
     * @OA\Put(
     *     path="/domains/{name}",
     *     summary="Updates a domain.",
     *     description="Updates a domain. Only supplied fields will be updated, existing won't be affected.",
     *     tags={"Domains"},
     *     @OA\Response(response="200", description="OK"),
     *     @OA\Response(response = "401", description = "API key is missing or invalid."),
     *     @OA\Response(response="404", description="Domain not found."),
     *     security={
     *         {"Authorization":{"read":"write"}}
     *     }
     * )
     * @OA\Delete (
     *     path="/domains/{name}",
     *     summary="Deletes a domain.",
     *     description="Deletes a domain.",
     *     tags={"Domains"},
     *     @OA\Response(response="200", description="OK"),
     *     @OA\Response(response = "401", description = "API key is missing or invalid."),
     *     @OA\Response(response="404", description="Domain not found."),
     *     security={
     *         {"Authorization":{"read":"write"}}
     *     }
     * )
     * @param string $requestMethod
     * @param array $uri
     *
     * @return void
     */

    #[
        OAT\Get(
            path: '/domains/{name}',
            operationId: 'getSingleDomain',
            description: 'Returns information of a single domain specified by its domain name.',
            summary: 'Returns a single domain.',
            security: [
            ],
            tags: ['Domains'],
            parameters: [
                new OAT\Parameter(name: 'name', in: 'path', required: true, schema: new OAT\Schema(type: 'string')),
            ],
            responses: [
                new OAT\Response(
                    response: 200,
                    description: 'OK'
                ),
                new OAT\Response(
                    response: 401,
                    description: 'API key is missing or invalid.'
                ),
                new OAT\Response(
                    response: 404,
                    description: 'Domain not found.'
                )]

        )]
    public function handleRequest(string $requestMethod, array $uri): void
    {
        $this->logger->debug(message: "Request: $requestMethod $uri[1]");

        $this->requestMethod = strtoupper(string: $requestMethod);
        $this->uri = $uri;

        $command = $this->uri[2];

        if (empty($command) || !(($command == 'domains') || ($command == 'ping') || ($command == 'apidoc') || ($command == 'dyndns'))) {
            $this->status = "404 Not Found";
            $this->message = "Endpoint not found.";
        } else {
            try {
                match ($command) {
                    'dyndns' => $this->handleDynDNS(),
                    'ping' => $this->handlePing(),
                    'domains' => $this->handleDomains(),
                };
            } catch (UnhandledMatchError) {
                $this->status = '400 Bad Request';
                $this->message = 'Unknown path: ' . $command;
            }
        }

        if (!empty($this->status)) {
            header(header: $_SERVER['SERVER_PROTOCOL'] . ' ' . $this->status);
        }

        if (!empty($this->response)) {
            echo json_encode(value: [
                'response' => $this->response
            ]);
        } elseif (!empty($this->result)) {
            echo json_encode(value: [
                'result' => $this->result
            ]);
        } elseif (!empty($this->message)) {
            echo json_encode(value: [
                'message' => $this->message
            ]);
        } else {
            echo json_encode(value: [
                'message' => $this->message ?? 'Error: No message.'
            ]);
        }
    }


    /**
     * @return bool
     */
    private function checkPassword(): bool
    {
        $headers = array_change_key_case(array: getallheaders(), case: CASE_UPPER);
        $apiKey = $headers['X-API-KEY'] ?? '';

        if (empty($apiKey)) {
            $this->status = "401 Unauthorized";
            $this->message = "API key is missing.";
            return false;
        } else {
            [$prefix,] = explode(separator: '.', string: $apiKey);
            if ($apiResult = $this->apikeyRepository->findByPrefix(prefix: $prefix)) {
                $encryptedHash = $apiResult->getApikey();
                $encryptionKey = $this->configController->getConfig(configKey: 'encryptionKey');
                $decryptedHash = $this->encryptionController->safeDecrypt(encrypted: $encryptedHash, key: $encryptionKey);

                if (!password_verify(password: $apiKey, hash: $decryptedHash)) {
                    $this->status = "401 Unauthorized";
                    $this->message = "API key mismatch.";
                    return false;
                }
            } else {
                $this->status = "401 Unauthorized";
                $this->message = "Invalid API key.";
                return false;
            }
        }
        return true;
    }


    /**
     * @return void
     */
    private function handleDomainsGetRequest(): void
    {
        $name = $this->uri[3] ?? '';

        if ($name == 'name') {
            if ($result = $this->domainRepository->findByName(name: $this->uri[4])) {
                $domain = [
                    'id' => $result->getId(),
                    'name' => $result->getName(),
                    'panel' => $result->getPanel()
                ];
                $this->result = $domain;
            } else {
                $this->status = "404 Not Found ";
                $this->message = "The specified domain was not found.";
            }
        } else {
            if (empty($name)) {
                $this->handleAllDomainsGetRequest();
            } else {
                $id = intval(value: $name);
                if ($id > 0) {
                    if ($result = $this->domainRepository->findById(id: $id)) {
                        $domain = [
                            'id' => $result->getId(),
                            'name' => $result->getName(),
                            'panel' => $result->getPanel()
                        ];
                        $this->result = $domain;
                    } else {
                        $this->status = "404 Not Found ";
                        $this->message = "The specified domain was not found.";
                    }
                } else {
                    $this->status = "400 Bad Request";
                    $this->message = "You need to supply an ID or user the /domain/name/<name> path.";

                }
            }
        }
    }


    /**
     * @return void
     */
    private function handleDomainsPostRequest(): void
    {
        $name = $_POST['name'] ?? '';
        $panel = $_POST['panel'] ?? '';
        if (empty($name)) {
            $this->status = "400 Bad Request";
            $this->message = "A name is required";
        } else {
            if (empty($panel)) {
                $this->status = "400 Bad Request";
                $this->message = "A panel ID is required.";
            } else {
                if ($this->domainRepository->findByName(name: $name)) {
                    $this->status = "400 Bad request";
                    $this->message = "Domain: $name already exists.";
                } else {
                    $domain = new Domain(name: $name, panel: $panel);
                    if ($result = $this->domainRepository->insert(domain: $domain)) {
                        $this->status = "201 Created";
                        $this->domainController->createSlaveZoneFile(domain: $domain);
                    } else {
                        $this->status = "500 Server error";
                    }
                    $this->message = $result;
                }
            }
        }
    }


    /**
     * @return void
     */
    private function handleDomainsPutRequest(): void
    {
        $putData = fopen(filename: 'php://input', mode: 'r');
        $data = fread(stream: $putData, length: 8192);
        $params = explode(separator: '&', string: $data);

        foreach ($params as $param) {
            [$key, $value] = explode(separator: '=', string: $param);
            $put[$key] = $value;
        }
        $id = $put['id'] ?? 0;
        $name = $put['name'] ?? '';
        $panel = $put['panel'] ?? "";

        if ($id == 0) {
            $this->status = "400 Bad Request";
            $this->message = "An ID is required";
        } else {
            if (!$this->domainRepository->findByID(id: $id)) {
                $this->status = "404 Not Found";
                $this->message = "Domain with ID : $id doesn't exist.";
            } else {
                $domain = new Domain(name: $name, panel: $panel, id: $id);
                $this->domainRepository->update(domain: $domain);
                $this->status = "201 Updated";
                $this->message = "201 Updated";
                $this->domainController->createSlaveZoneFile(domain: $domain);
            }
        }
    }


    /**
     * @return void
     */
    private function handleDomainsDeleteRequest(): void
    {
        $deleteData = fopen(filename: 'php://input', mode: 'r');
        $data = fread(stream: $deleteData, length: 512);
        $params = explode(separator: '&', string: $data);

        foreach ($params as $param) {
            [$key, $value] = explode(separator: '=', string: $param);
            $delete[$key] = $value;
        }

        $id = $delete['id'] ?? 0;

        if ($id == 0) {
            $this->status = "400 Bad Request";
            $this->message = "You need to supply an ID.";
        } else {

            if (!$domain = $this->domainRepository->findByID(id: $id)) {
                $this->status = "400 Bad Request";
                $this->message = "There is no domain with ID $id.";
            } else {
                $this->domainRepository->delete(domain: $domain);
                $this->status = "204 No content.";
                $this->message = "The domain $id has been deleted.";
            }
        }
    }


    private function handleDynDNS(): void
    {
        $this->logger->debug(message: 'handleDynDNS()');

        if ($this->checkPassword()) {
            $host = $this->uri[3] ?? '';

            if (empty($host)) {
                $this->status = '400 Bad Request';
            } else {
                $a = $_POST['a'] ?? '';
                $aaaa = $_POST['aaaa'] ?? '';

                if (empty($a) && empty($aaaa)) {
                    $address = $_SERVER['REMOTE_ADDR'];

                    if (filter_var(value: $address, filter: FILTER_VALIDATE_IP, options: FILTER_FLAG_IPV6)) {
                        $aaaa = $address;
                    } else {
                        $a = $address;
                    }
                }

                $this->logger->debug(message: 'a: ' . $a);
                $this->logger->debug(message: 'aaaa: ' . $aaaa);


                $domainName = $this->getDomain(host: $host);
                $hostName = str_replace(search: '.' . $domainName, replace: '', subject: $host);
                if (!$domain = $this->domainRepository->findByName(name: $domainName)) {
                    $this->status = '404 Not Found';
                    $this->message = 'Domain ' . $domainName . ' not found';
                } else {
                    // check if address has changed
                    if ($dynDNS = $this->dynDNSRepository->findByName(name: $host)) {
                        $this->logger->debug(message: 'found host: ' . $host);
                        $this->logger->debug(message: "a: $a");
                        $this->logger->debug(message: "aaaa: $aaaa");

                        $ipChanged = false;

                        if (!empty($a)) {
                            if ($a != $dynDNS->getA()) {
                                $this->logger->debug(message: $a . ' != ' . $dynDNS->getA());
                                $dynDNS->setA(a: $a);
                                $ipChanged = true;
                            }
                        }

                        if (!empty($aaaa)) {
                            if ($aaaa != $dynDNS->getAaaa()) {
                                $this->logger->debug(message: $aaaa . ' != ' . $dynDNS->getAaaa());
                                $dynDNS->setAaaa(aaaa: $aaaa);
                                $ipChanged = true;
                            }
                        }

                        if ($ipChanged) {
                            $this->dynDNSRepository->update(dynDNS: $dynDNS);
                        }
                    } else {
                        $dynDNS = new DynDNS(name: $host, a: $a, aaaa: $aaaa);
                        $this->dynDNSRepository->insert(dynDNS: $dynDNS);
                        $ipChanged = true;
                    }


                    if ($ipChanged) {
                        $panel = $this->panelRepository->findByName(name: $domain->getPanel());

                        if (!empty($panel->getAaaa())) {
                            $domainData = $this->apiController->sendCommand(
                                requestType: 'GET',
                                serverName: $panel->getName(),
                                versionIP: 6,
                                apiKey: $panel->getApikey(),
                                command: 'domains/name/' . $domainName,
                                serverType: 'panel');
                        } else {
                            $domainData = $this->apiController->sendCommand(
                                requestType: 'GET',
                                serverName: $panel->getName(),
                                versionIP: 4,
                                apiKey: $panel->getApikey(),
                                command: 'domains/name/' . $domainName,
                                serverType: 'panel');
                        }

                        $domainDecodedData = json_decode(json: $domainData['data']);
                        $domainID = $domainDecodedData->id;

                        if (!empty($panel->getAaaa())) {
                            $dnsData = $this->apiController->sendCommand(
                                requestType: 'GET',
                                serverName: $panel->getName(),
                                versionIP: 6,
                                apiKey: $panel->getApikey(),
                                command: 'dns/' . $domainID,
                                serverType: 'panel');
                        } else {
                            $dnsData = $this->apiController->sendCommand(
                                requestType: 'GET',
                                serverName: $panel->getName(),
                                versionIP: 4,
                                apiKey: $panel->getApikey(),
                                command: 'dns/' . $domainID,
                                serverType: 'panel');
                        }

                        $dnsDataDecoded = json_decode(json: $dnsData['data']);
                        $soa = $dnsDataDecoded->records->soa;
                        $others = $dnsDataDecoded->records->other;

                        $hostFound = false;

                        $updateHost = function (object $host) use ($hostName, $a, $aaaa, &$hostFound) {
                            if ($host->host == $hostName) {
                                $hostFound = true;
                                if ($host->type == 'A') {
                                    if (!empty($a)) {
                                        $host->value = $a;
                                    }
                                } else {
                                    if (!empty($aaaa)) {
                                        $host->value = $aaaa;
                                    }
                                }
                            }
                        };

                        array_map(callback: $updateHost, array: $others);

                        if ($hostFound) {
                            $newDnsData = json_encode(value: [
                                'records' => [
                                    'soa' => $soa,
                                    'other' => $others
                                ]
                            ]);

                            if (!empty($panel->getAaaa())) {
                                $result = $this->apiController->sendCommand(
                                    requestType: 'PUT',
                                    serverName: $panel->getName(),
                                    versionIP: 6,
                                    apiKey: $panel->getApikey(),
                                    command: 'dns/' . $domainID,
                                    serverType: 'panel',
                                    body: json_decode(json: $newDnsData, associative: true)
                                );
                            } else {
                                $result = $this->apiController->sendCommand(
                                    requestType: 'PUT',
                                    serverName: $panel->getName(),
                                    versionIP: 4,
                                    apiKey: $panel->getApikey(),
                                    command: 'dns/' . $domainID,
                                    serverType: 'panel',
                                    body: json_decode(json: $newDnsData, associative: true)
                                );
                            }
                            if ($result['header'] == 200) {
                                $this->status = '200 OK';
                                $this->message = 'DynDNS host successfully updated';
                            }
                        } else {
                            $this->status = '404 Not Found';
                            $this->message = 'Host ' . $hostName . ' not found';
                        }
                    } else {
                        $this->status = '204 No content';
                        $this->message = 'No content';
                    }
                }
            }
        }
    }


    /**
     * @param String $host
     *
     * @return string
     */
    private function getDomain(string $host): string
    {
        $host = strtolower(string: trim(string: $host));
        $count = substr_count(haystack: $host, needle: '.');
        if ($count == 2) {
            if (strlen(string: explode(separator: '.', string: $host)[1]) > 3) {
                $host = explode(separator: '.', string: $host, limit: 2)[1];
            }
        } elseif ($count > 2) {
            $host = $this->getDomain(host: explode(separator: '.', string: $host, limit: 2)[1]);
        }
        return $host;
    }


}
