/*
 * telega-server.c --- Bridge between Emacs and TDLib.
 *
 * Copyright (C) 2016-2025 by Zajcev Evgeny
 *
 * Author: Zajcev Evgeny <zevlg@yandex.ru>
 *
 * telega is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * telega is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with telega.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <stdbool.h>
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#include <td/telegram/td_json_client.h>
#include <td/telegram/td_log.h>

#include "telega-dat.h"
#ifdef WITH_VOIP
extern const char* telega_voip_version(void);
extern int telega_voip_cmd(const char* json);
#endif /* WITH_VOIP */

#ifdef WITH_APPINDICATOR
extern void telega_appindicator_send(const char* json);
extern void* telega_appindicator_loop(void* data);
extern void telega_appindicator_stop(void);
#endif /* WITH_APPINDICATOR */

void pngext_usage(char* prog);
void pngext_main(int ac, char** av);

/*
 * Input/Output Protocol:
 * ~~~~~~~~~~~~~~
 *   <COMMAND> <SPACE> <PLIST-LEN> <NEWLINE>
 *   <PLIST of PLIST-LEN length> <NEWLINE>
 *
 * COMMAND is one of `send', `event' or `error'
 * `event' and `error' is used for output
 *
 * If VOIP support is compiled in (make WITH_VOIP=true) then also
 * `voip' command available
 *
 * If support for appindicator is compiled in, then `appindicator'
 * command is also available.
 *
 * For example:
 *   event 105
 *   (:@type "updateAuthorizationState" :authorization_state (:@type "authorizationStateWaitTdlibParameters"))
 *
 *   send 109
 *   (:@type "getTextEntities" :text "@telegram /test_command https://telegram.org telegram.me" :@extra ["5" 7.0])
 *
 */

char* logfile = NULL;
size_t logfile_size = 4 * 1024 * 1024;
int verbosity = 5;
const char* version = "1.2.0";

/* true when stdin_loop() is running */
volatile bool server_running;

int parse_mode = 0;
#define PARSE_MODE_JSON 1
#define PARSE_MODE_PLIST 2

int optimize = 0;

#ifdef WITH_ZLIB
#include <zlib.h>
bool zlib_enabled = false;
#endif /* WITH_ZLIB */

void
usage(char* prog)
{
        printf("Version %s", version);
#ifdef WITH_VOIP
        printf(", with VOIP tgvoip v%s", telega_voip_version());
#endif /* WITH_VOIP */
#ifdef WITH_APPINDICATOR
        printf(", with appindicator");
#endif /* WITH_APPINDICATOR */
#ifdef WITH_ZLIB
        printf(", with zlib");
#endif /* WITH_ZLIB */
        printf("\n");
        printf("usage: %s"
#ifdef WITH_ZLIB
               " [-z]"
#endif /* WITH_ZLIB */
               " [-jp] [-L SIZE] [-l FILE] [-v LVL] [-O OPT] [-h]\n",
               prog);
        printf("\t-L SIZE    Log file size in bytes (default=%zu)\n",
               logfile_size);
        printf("\t-l FILE    Log to FILE (default=stderr)\n");
        printf("\t-v LVL     Verbosity level (default=5)\n");
        printf("\t-O OPT     Optimizations to use (default=0)\n");
#ifdef WITH_ZLIB
        printf("\t-z         Compress output with zlib\n");
#endif /* WITH_ZLIB */
        printf("\t-j         Parse json from stdin and exit\n");
        printf("\t-p         Parse plist from stdin and exit\n");

        printf("\n---- PNG extracting functionality ----\n");
        pngext_usage(prog);
        exit(0);
}

void
telega_output(const char* otype, const char* str)
{
        if (verbosity > 4) {
                fprintf(stderr, "[telega-server] "
                        "OUTPUT %s: %s\n", otype, str);
        }

        printf("%s %zu\n%s\n", otype, strlen(str), str);
        fflush(stdout);
}

static void
output_json_prepare(const char* otype, const char* json,
                    struct telega_dat* output)
{
        if (verbosity > 4) {
                fprintf(stderr, "[telega-server] OUTPUT %s: %s\n", otype, json);
        }

        if ((optimize & OPTIMIZE_EMPTY_OK)
            && !strncmp("{\"@type\":\"ok\"}", json, 15))
        {
                if (verbosity > 4)
                        fprintf(stderr,
                                "[telega-server] OPTIMIZE_EMPTY_OK\n");
                return;
                /* NOT REACHED */
        }

        if ((optimize & OPTIMIZE_NOTIFICATIONS)
            && !strncmp("{\"@type\":\"updateHavePendingNotifications\"",
                        json, 41))
        {
                if (verbosity > 4)
                        fprintf(stderr,
                                "[telega-server] OPTIMIZE_NOTIFICATIONS\n");
                return;
                /* NOT REACHED */
        }

        size_t json_len = strlen(json);
        struct telega_dat json_src = {
                .cap = json_len,
                .start = 0,
                .end = json_len,
                .data = (char*)json,
                .free_data = NULL
        };

        assert(output->end == 0);

        /* Reserve 64 bytes for output header */
        tdat_ensure(output, 64);
        output->start = output->end = 64;

        tdat_json_value(&json_src, output);

        char hdr_buf[64+1];
        int hdr_len = snprintf(hdr_buf, 64+1, "%s %zu\n",
                               otype, tdat_len(output));
        assert(hdr_len > 0 && hdr_len < output->start);
        output->start -= hdr_len;

        /* copy header not including trailing '\0' */
        memcpy(tdat_start(output), hdr_buf, hdr_len);

        tdat_append1(output, "\n");
}

static void*
tdlib_loop(void* cln)
{
        struct telega_dat output = TDAT_INIT;

        while (server_running) {
                const char *res = td_json_client_receive(cln, 10.0);
                if (res) {
                        tdat_reset(&output);
                        output_json_prepare("event", res, &output);
                        fwrite(tdat_start(&output),
                               1, tdat_len(&output), stdout);
                        fflush(stdout);
                }
        }

        tdat_drop(&output);
        return NULL;
}

#ifdef WITH_ZLIB
static void*
tdlib_zlib_loop(void* cln)
{
        struct telega_dat output = TDAT_INIT;
        struct telega_dat zlib_dst = TDAT_INIT;

        while (server_running) {
                const char *res = td_json_client_receive(cln, 10.0);
                if (res == NULL)
                        continue;

                /* Fetch all pending events at once */
                tdat_reset(&output);
                do {
                        output_json_prepare("event", res, &output);
                } while ((res = td_json_client_receive(cln, 0)));

                tdat_reset(&zlib_dst);
                size_t zlen = tdat_zlib_deflate(&output, &zlib_dst);
                printf("zlib %zu\n", zlen);
                size_t wsize = fwrite(tdat_start(&zlib_dst),
                                      1, tdat_len(&zlib_dst), stdout);
                assert(wsize == zlen);
                printf("\n");
                fflush(stdout);
        }

        tdat_drop(&zlib_dst);
        tdat_drop(&output);
        return NULL;
}
#endif /* WITH_ZLIB */

/*
 * NOTE: Emacs sends HUP when associated buffer is killed
 * kind of graceful exit
 */
static void
on_sighup(int sig)
{
        close(0);
}

static void
stdin_loop(void* td_cln)
{
        struct telega_dat plist_src = TDAT_INIT;
        struct telega_dat json_dst = TDAT_INIT;
        char cmdline[33];

        signal(SIGHUP, on_sighup);
        while (fgets(cmdline, 33, stdin)) {
                cmdline[32] = '\0';

                char cmd[33];
                size_t cmdsz = 0;
                if (2 != sscanf(cmdline, "%s %zu\n", cmd, &cmdsz)) {
                        fprintf(stderr, "[telega-server] "
                                "Unexpected cmdline format: %s\n", cmdline);
                        continue;
                }
                if (cmdsz > 10 * 1024 * 1024) {
                        fprintf(stderr, "[telega-server] cmd size = %zu is too large",
                                cmdsz);
                        continue;
                }

                tdat_ensure(&plist_src, cmdsz);

                /* read including newline */
                size_t rc = fread(plist_src.data, 1, cmdsz + 1, stdin);
                if (rc != cmdsz + 1) {
                        /* EOF or error */
                        fprintf(stderr, "[telega-server] "
                                "fread() error: %d\n", ferror(stdin));
                        break;
                }
                plist_src.end = cmdsz + 1;
                tdat_append1(&plist_src, "\0");
                if (verbosity > 4) {
                        fprintf(stderr, "[telega-server] "
                                "INPUT (cmd=%s): %s\n", cmd, plist_src.data);
                }

                tdat_plist_value(&plist_src, &json_dst);
                tdat_append1(&json_dst, "\0");

                if (!strcmp(cmd, "send")) {
                        td_json_client_send(td_cln, tdat_start(&json_dst));
                } else if (!strcmp(cmd, "voip")) {
#ifdef WITH_VOIP
                        telega_voip_cmd(tdat_start(&json_dst));
#endif /* WITH_VOIP */
                } else if (!strcmp(cmd, "appindicator")) {
#ifdef WITH_APPINDICATOR
                        /* Strip leading/trailing " */
                        if (tdat_len(&json_dst) > 1) {
                                if (tdat_start(&json_dst)[0] == '"')
                                        tdat_drain(&json_dst, 1);
                                json_dst.end -= 2;
                                tdat_append1(&json_dst, "\0");
                        }
                        telega_appindicator_send(tdat_start(&json_dst));
#endif /* WITH_APPINDICATOR */
                } else {
                        char error[128];
                        snprintf(error, 128, "\"Unknown cmd `%s'\"", cmd);
                        telega_output("error", error);

                        fprintf(stderr, "[telega-server] "
                                "Unknown command: %s\n", cmd);
                }

                tdat_reset(&plist_src);
                tdat_reset(&json_dst);
        }

        tdat_drop(&plist_src);
        tdat_drop(&json_dst);
}

static void
parse_stdin(void)
{
        struct telega_dat src = TDAT_INIT;

#define RDSIZE 1024
        tdat_ensure(&src, RDSIZE);

        ssize_t rlen;
        while ((rlen = read(0, &src.data[src.end], RDSIZE)) > 0) {
                src.end += rlen;
                tdat_ensure(&src, RDSIZE);
        }
#undef RDSIZE
        tdat_append1(&src, "\0");

        struct telega_dat dst = TDAT_INIT;
        if (parse_mode == PARSE_MODE_JSON)
                tdat_json_value(&src, &dst);
        else
                tdat_plist_value(&src, &dst);
        tdat_append1(&dst, "\0");

        printf("%s\n", dst.data);

        tdat_drop(&src);
        tdat_drop(&dst);
}

void
telega_set_verbosity(int verbosity)
{
        char req[1024];
        snprintf(req, 1024, "{\"@type\":\"setLogVerbosityLevel\","
                 "\"new_verbosity_level\":%d}", verbosity);
        td_json_client_execute(NULL, req);
}

void
telega_set_logfile(char* logfile, size_t logfile_size)
{
        char req[1024];
        snprintf(req, 1024, "{\"@type\":\"setLogStream\","
                 "\"log_stream\":{\"@type\":\"logStreamFile\","
                 "\"path\":\"%s\", \"max_file_size\":%zu,"
                 "\"redirect_stderr\":true}}",
                 logfile, logfile_size);
        td_json_client_execute(NULL, req);
}

int
main(int ac, char** av)
{
        int ch;
        while ((ch = getopt(ac, av, "O:L:E:R:f:jpl:v:hz")) != -1) {
                switch (ch) {
#ifdef WITH_ZLIB
                case 'z':
                        zlib_enabled = true;
                        break;
#endif /* WITH_ZLIB */
                case 'O':
                        optimize = atoi(optarg);
                        break;
                case 'v':
                        verbosity = atoi(optarg);
                        telega_set_verbosity(verbosity);
                        break;
                case 'L':
                        logfile_size = atoi(optarg);
                        break;
                case 'l':
                        logfile = optarg;
                        break;
                case 'j':
                        parse_mode = PARSE_MODE_JSON;
                        break;
                case 'p':
                        parse_mode = PARSE_MODE_PLIST;
                        break;
                case 'E':
                case 'R':
                case 'f':
                        pngext_main(ac, av);
                        return 0;
                        /* NOT REACHED */
                case 'h':
                case '?':
                default:
                        usage(av[0]);
                        /* NOT REACHED */
                }
        }

        if (logfile) {
                telega_set_logfile(logfile, logfile_size);
        } else {
                /*
                 * If log file is disable, we disable debug output as
                 * well, because stderr redirects only to logfile.
                 *
                 * Fixes https://t.me/emacs_telega/50428
                 */
                verbosity = 0;
        }

        if (parse_mode) {
                parse_stdin();
                return 0;
                /* NOT REACHED */
        }

        server_running = true;

        void* tdlib_cln = td_json_client_create();
        assert(tdlib_cln != NULL);
        pthread_t td_thread;
        int rc = pthread_create(&td_thread, NULL,
#ifdef WITH_ZLIB
                                zlib_enabled ? tdlib_zlib_loop :
#endif /* WITH_ZLIB */
                                tdlib_loop,
                                tdlib_cln);
        assert(rc == 0);

#ifdef WITH_APPINDICATOR
        pthread_t appind_thread;
        rc = pthread_create(&appind_thread, NULL,
                            telega_appindicator_loop, NULL);
        assert(rc == 0);
#endif /* WITH_APPINDICATOR */

        stdin_loop(tdlib_cln);
        /* Gracefully stop the tdlib_loop */
        server_running = false;

        td_json_client_send(tdlib_cln, "{\"@type\":\"close\"}");
        rc = pthread_join(td_thread, NULL);
        assert(rc == 0);
        td_json_client_destroy(tdlib_cln);

#ifdef WITH_APPINDICATOR
        telega_appindicator_stop();
        rc = pthread_join(appind_thread, NULL);
        assert(rc == 0);
#endif /* WITH_APPINDICATOR */

        return 0;
}
