//
// Created by xajhuang on 2022/5/10.
//
#include <zlog.h>
#include <uv/unix.h>
#include <uv.h>
#include <uthash/utlist.h>
#include "pppoe_session.h"
#include "netif/ppp/ppp.h"
#include "netif/ppp/pppoe.h"
#include "netif/pppoeif.h"
#include "misc.h"
#include "user_errno.h"
#include "netif/ppp/pppapi.h"
#include "config.h"
#include "msg_queue.h"
#include "vxlan_pkg.h"
#include "netif/pcapif.h"
#include "s2j/cJSON.h"

typedef struct PPPOE_CACHE {
    PPPPOE_SESSION_DATA pSessionData;
    int                 nextStatus;
    PUSER_INFO_CONTEXT  pUser;

    struct PPPOE_CACHE *next, *prev;
} PPPOE_CACHE, *PPPPOE_CACHE;

struct PPPOE_ERR_INFO_ {
    int         errid;
    const char *errmsg;
} g_pppoeErr[] = {
    {PPPERR_NONE, "No error"},
    {PPPERR_PARAM, "Invalid parameter"},
    {PPPERR_OPEN, "Unable to open PPP session"},
    {PPPERR_DEVICE, "Invalid I/O device for PPP"},
    {PPPERR_ALLOC, "Unable to allocate resources"},
    {PPPERR_USER, "User interrupt"},
    {PPPERR_CONNECT, "Connection lost"},
    {PPPERR_AUTHFAIL, "Failed authentication challenge"},
    {PPPERR_PROTOCOL, "Failed to meet protocol"},
    {PPPERR_PEERDEAD, "Connection timeout"},
    {PPPERR_IDLETIMEOUT, "Idle Timeout"},
    {PPPERR_CONNECTTIME, "Max connect time reach max time"},
    {PPPERR_LOOPBACK, "Loopback detect"},
};

static struct netif *g_rawSocketIf = NULL;
static PPPPOE_CACHE  g_pPPPCache   = NULL;
static PPPPOE_CACHE  g_pPPPDelete  = NULL;
static uv_rwlock_t   g_cacheLock;

static void pppLinkStatusCallback(ppp_pcb *pcb, int errCode, void *ctx) {
    PPPPOE_CACHE        pCache;
    struct netif       *pppif = ppp_netif(pcb);
    struct pppoe_softc *sc    = (struct pppoe_softc *)pcb->link_ctx_cb;
    PUSER_INFO_CONTEXT  pUser = (PUSER_INFO_CONTEXT)ctx;

    switch (errCode) {
        case PPPERR_NONE: { /* No error. */
            pCache = (PPPPOE_CACHE)malloc(sizeof(PPPOE_CACHE));
            dzlog_info("<%p> PPPoE user(%05d:%s) connect server succeeded[%08X], Session: %04X\n",
                       pcb,
                       pUser->userid,
                       pUser->user_info.pppoe_user,
                       pcb->lcp_gotoptions.magicnumber,
                       sc->sc_session);
#if LWIP_IPV4
            pUser->session.data.sessionId = sc->sc_session;
            strncpy(pUser->session.data.clientIp, ip4addr_ntoa(netif_ip4_addr(pppif)), MAX_IP_V4_STR);
            strncpy(pUser->session.data.clientGw, ip4addr_ntoa(netif_ip4_gw(pppif)), MAX_IP_V4_STR);
            strncpy(pUser->session.data.clientMask, ip4addr_ntoa(netif_ip4_netmask(pppif)), MAX_IP_V4_STR);
            sprintf(pUser->session.data.clientMac,
                    "%02X:%02X:%02X:%02X:%02X:%02X",
                    pUser->user_info.mac_addr[0],
                    pUser->user_info.mac_addr[1],
                    pUser->user_info.mac_addr[2],
                    pUser->user_info.mac_addr[3],
                    pUser->user_info.mac_addr[4],
                    pUser->user_info.mac_addr[5]);
            dzlog_info("   our_ipaddr  = %s\n", pUser->session.data.clientIp);
            dzlog_info("   his_ipaddr  = %s\n", pUser->session.data.clientGw);
            dzlog_info("   netmask     = %s\n", pUser->session.data.clientMask);
#endif /* LWIP_IPV4 */
#if LWIP_DNS
            dzlog_info("   dns1        = %s\n", ipaddr_ntoa(dns_getserver(0)));
            dzlog_info("   dns2        = %s\n", ipaddr_ntoa(dns_getserver(1)));
#endif /* LWIP_DNS */
#if PPP_IPV6_SUPPORT
            dzlog_info("   our6_ipaddr = %s\n", ip6addr_ntoa(netif_ip6_addr(pppif, 0)));
#endif /* PPP_IPV6_SUPPORT */

            if (pCache) {
                pCache->pSessionData = &pUser->session.data;
                pCache->pUser        = pUser;
                uv_rwlock_wrlock(&g_cacheLock);
                LL_APPEND(g_pPPPCache, pCache);
                uv_rwlock_wrunlock(&g_cacheLock);
            }
            pUser->session.status = STATUS_TASK_CONNECTED;
            break;
        }
        case PPPERR_PARAM:
        case PPPERR_AUTHFAIL:
        case PPPERR_PROTOCOL:
            dzlog_error("<%p> pppLinkStatusCallback: %s(%d)",
                        pcb,
                        g_pppoeErr[errCode].errmsg,
                        g_pppoeErr[errCode].errid);
            if (pUser->session.status != STATUS_TASK_DELETE) {
                if (pUser->session.status == STATUS_TASK_CONNECTED) {
                    pCache = (PPPPOE_CACHE)malloc(sizeof(PPPOE_CACHE));

                    if (pCache) {
                        pCache->pSessionData = &pUser->session.data;
                        pCache->pUser        = pUser;
                        pCache->nextStatus   = STATUS_TASK_ERROR;
                        uv_rwlock_wrlock(&g_cacheLock);
                        LL_APPEND(g_pPPPDelete, pCache);
                        uv_rwlock_wrunlock(&g_cacheLock);
                    } else {
                        pUser->session.status = STATUS_TASK_ERROR;
                    }
                } else {
                    pUser->session.status = STATUS_TASK_ERROR;
                }
            }
            break;
        case PPPERR_USER:
            dzlog_info("User(%05d:%s) disconnect\n", pUser->userid, pUser->user_info.pppoe_user);

            pCache = (PPPPOE_CACHE)malloc(sizeof(PPPOE_CACHE));
            if (pCache) {
                pCache->pSessionData = &pUser->session.data;
                pCache->pUser        = pUser;
                pCache->nextStatus   = pUser->session.status;
                uv_rwlock_wrlock(&g_cacheLock);
                LL_APPEND(g_pPPPDelete, pCache);
                uv_rwlock_wrunlock(&g_cacheLock);
            }

            break;
        case PPPERR_OPEN:
        case PPPERR_DEVICE:
        case PPPERR_ALLOC:
        case PPPERR_CONNECT:
        case PPPERR_PEERDEAD:
        case PPPERR_IDLETIMEOUT:
        case PPPERR_CONNECTTIME:
        case PPPERR_LOOPBACK:
            dzlog_error("<%p> pppLinkStatusCallback: %s(%d)\n",
                        pcb,
                        g_pppoeErr[errCode].errmsg,
                        g_pppoeErr[errCode].errid);
            if (pUser->session.status != STATUS_TASK_DELETE) {
                if (pUser->session.status == STATUS_TASK_CONNECTED) {
                    pCache = (PPPPOE_CACHE)malloc(sizeof(PPPOE_CACHE));
                    if (pCache) {
                        pCache->pSessionData = &pUser->session.data;
                        pCache->pUser        = pUser;
                        pCache->nextStatus   = STATUS_TASK_ERROR;
                        uv_rwlock_wrlock(&g_cacheLock);
                        LL_APPEND(g_pPPPDelete, pCache);
                        uv_rwlock_wrunlock(&g_cacheLock);
                    } else {
                        pUser->session.status = STATUS_TASK_DISCONNECTED;
                    }
                } else {
                    pUser->session.status = STATUS_TASK_DISCONNECTED;
                }
            }
            break;
        default: {
            printf("<%p> pppLinkStatusCallback: unknown errCode %d\n", pcb, errCode);
            break;
        }
    }
}

_Noreturn void sessionCalcCb(void *UNUSED(pArg)) {
    do {
        PUSER_INFO_CONTEXT pUserList = get_all_user_by_id();
        PUSER_INFO_CONTEXT pUser, pTmp;

        uv_rwlock_rdlock(get_user_lock());
        HASH_ITER(hh_id, pUserList, pUser, pTmp) {
            PPPPOE_SESSION pSession = &pUser->session;

            switch (pSession->status) {
                case STATUS_TASK_INIT:
                    if (pppoe_session_create(pUser) == ERR_SUCCESS) {
                        dzlog_debug("User(%05d:%s) init pppoe session\n", pUser->userid, pUser->user_info.pppoe_user);
                        pSession->status = STATUS_TASK_DIAL;
                    }
                    break;
                case STATUS_TASK_DIAL:
                    if (pUser->session.retry.timeout == 0) {
                        dzlog_debug("User(%05d:%s) connect PPPoE server\n", pUser->userid, pUser->user_info.pppoe_user);
                        pppapi_connect(pSession->ppp, 0);
                        pUser->session.retry.timeout = time(NULL) + PPPOE_MAX_TIMEOUT;
                    } else if (time(NULL) > pUser->session.retry.timeout) {
                        pUser->session.retry.timeout = 0;
                    }
                    break;
                case STATUS_TASK_ERROR:
                    if (pUser->session.retry.timeout == 0) {
                        dzlog_error("User(%05d:%s) PPPoE dial error Invalid parameter\n",
                                    pUser->userid,
                                    pUser->user_info.pppoe_user);
                        // 10秒后尝试重新拨号
                        pUser->session.retry.timeout = time(NULL) + (PPPOE_MAX_TIMEOUT / 2);
                        pUser->session.retry.count   = 0;
                    } else if (time(NULL) > pUser->session.retry.timeout) {
                        dzlog_warn("User(%05d:%s) retry dial %u times\n",
                                   pUser->userid,
                                   pUser->user_info.pppoe_user,
                                   pUser->session.retry.count);
                        pUser->session.retry.timeout = 0;
                        pUser->session.retry.count++;

                        // 下次重新拨号
                        pSession->status = STATUS_TASK_DIAL;
                    }
                    break;

                case STATUS_TASK_DISCONNECTED:
                    dzlog_error("User %s disconnect, auto reconnect\n", pUser->user_info.pppoe_user);
                    // 自动重新拨号
                    pSession->status = STATUS_TASK_DIAL;
                    break;

                case STATUS_TASK_DELETE:
                    if (pUser->session.retry.timeout == 0) {
                        dzlog_debug("User(%05d:%s) PPPoE deleted\n", pUser->userid, pUser->user_info.pppoe_user);
                        pppapi_close(pUser->session.ppp, 0);
                        pUser->session.retry.timeout = time(NULL);
                        pppapi_free(pUser->session.ppp);

                        user_info_delete(pUser->userid);
                    }
                    break;
#if 0
                case STATUS_TASK_CONNECTED:
                    if (pUser->session.retry.timeout == 0) {
                        pUser->session.retry.timeout = time(NULL) + 30;
                    } else if (time(NULL) > pUser->session.retry.timeout) {
                        dzlog_debug("User(%05d:%s) PPPoE disconnected\n", pUser->userid, pUser->user_info.pppoe_user);
                        pSession->status = STATUS_TASK_DELETE;
                        pppapi_close(pUser->session.ppp, 0);
                        pUser->session.retry.timeout = 0;
                    }

                    break;
#endif
                default:
                    break;
            }
        }
        uv_rwlock_rdunlock(get_user_lock());
        uv_sleep(1000);
    } while (TRUE);
}

_Noreturn void cacheCalcCb(void *UNUSED(pArg)) {
    do {
        PPPPOE_CACHE pCache, pTmp;
        int          count;

        uv_rwlock_rdlock(&g_cacheLock);
        LL_COUNT(g_pPPPCache, pCache, count);
        uv_rwlock_rdunlock(&g_cacheLock);

        if (count > 0) {
            const char *pJsonString;
            cJSON      *pRoot    = cJSON_CreateObject();
            cJSON      *pSession = cJSON_CreateArray();
            cJSON_AddStringToObject(pRoot, "message", "add-ywg-pppoe");

            uv_rwlock_wrlock(&g_cacheLock);
            LL_FOREACH_SAFE(g_pPPPCache, pCache, pTmp) {
                cJSON *pItem = cJSON_CreateObject();
                cJSON_AddNumberToObject(pItem, "sessionId", pCache->pSessionData->sessionId);
                cJSON_AddStringToObject(pItem, "clientIp", pCache->pSessionData->clientIp);
                cJSON_AddStringToObject(pItem, "clientGw", pCache->pSessionData->clientGw);
                cJSON_AddStringToObject(pItem, "clientMask", pCache->pSessionData->clientMask);
                cJSON_AddStringToObject(pItem, "clientMac", pCache->pSessionData->clientMac);
                cJSON_AddStringToObject(pItem, "localMac", pCache->pSessionData->svrBaseMac);
                cJSON_AddItemToArray(pSession, pItem);
                LL_DELETE(g_pPPPCache, pCache);
                free(pCache);
            }
            uv_rwlock_wrunlock(&g_cacheLock);
            cJSON_AddItemToObject(pRoot, "params", pSession);
            pJsonString = cJSON_PrintUnformatted(pRoot);
            mq_data_send_msg(pJsonString);
            cJSON_Delete(pRoot);
            free((void *)pJsonString);
        }

        uv_rwlock_rdlock(&g_cacheLock);
        LL_COUNT(g_pPPPDelete, pCache, count);
        uv_rwlock_rdunlock(&g_cacheLock);

        if (count > 0) {
            const char *pJsonString;
            cJSON      *pRoot    = cJSON_CreateObject();
            cJSON      *pSession = cJSON_CreateArray();
            cJSON_AddStringToObject(pRoot, "message", "remove-ywg-pppoe");

            uv_rwlock_wrlock(&g_cacheLock);
            LL_FOREACH_SAFE(g_pPPPDelete, pCache, pTmp) {
                cJSON *pItem = cJSON_CreateObject();
                cJSON_AddNumberToObject(pItem, "sessionId", pCache->pSessionData->sessionId);
                cJSON_AddStringToObject(pItem, "clientMac", pCache->pSessionData->clientMac);
                cJSON_AddItemToArray(pSession, pItem);
                pCache->pUser->session.status = pCache->nextStatus;
                LL_DELETE(g_pPPPDelete, pCache);
                free(pCache);
            }
            uv_rwlock_wrunlock(&g_cacheLock);
            cJSON_AddItemToObject(pRoot, "params", pSession);
            pJsonString = cJSON_PrintUnformatted(pRoot);
            mq_data_send_msg(pJsonString);
            cJSON_Delete(pRoot);
            free((void *)pJsonString);
        }

        uv_sleep(1000);
    } while (TRUE);
}

int pppoe_session_init() {
    static uv_thread_t uvThread, cacheThread;
    uv_rwlock_init(&g_cacheLock);

    g_rawSocketIf = bind_pcap_if(config_get_vxlan_nic_name(), config_get_vxlan_pkg_filter(), cfg_get_support_vxlan());

    if (g_rawSocketIf) {
        dzlog_info("Create hardware netif: <%p>\n", (void *)g_rawSocketIf);
    } else {
        dzlog_info("Create hardware error: <%p>\n", (void *)g_rawSocketIf);
    }

    // 启动Session状态机线程
    uv_thread_create(&uvThread, sessionCalcCb, NULL);
    uv_thread_create(&cacheThread, cacheCalcCb, NULL);
    return ERR_SUCCESS;
}

int pppoe_session_create(PUSER_INFO_CONTEXT pUser) {
    ppp_pcb      *ppp;
    struct netif *netif;
    struct netif *ppp_netif = (struct netif *)malloc(sizeof(struct netif));

    if (ppp_netif == NULL) {
        dzlog_error("Malloc %lu bytes memory error\n", sizeof(struct netif));
        return -ERR_MALLOC_MEMORY;
    }

    netif = create_pppoe_if(pUser);

    if (netif == NULL) {
        dzlog_error("Create PPPoE netif error: %u\n", pUser->userid);
        free((void *)ppp_netif);
        return -ERR_CREATE_PPPOE_NETIF;
    }

    pUser->session.nicif = netif;
    pUser->session.pppif = ppp_netif;

    ppp = pppapi_pppoe_create(pUser->session.pppif, pUser->session.nicif, NULL, NULL, pppLinkStatusCallback, pUser);

    if (ppp == NULL) {
        dzlog_error("Create PPPoE session error: %u\n", pUser->userid);
        netif_remove(netif);
        free((void *)ppp_netif);
        return -ERR_CREATE_PPP_SESSION;
    }

    pUser->session.ppp = ppp;

    ppp_set_auth(ppp, PPPAUTHTYPE_ANY, pUser->user_info.pppoe_user, pUser->user_info.pppoe_passwd);

    dzlog_debug("Create PPPoE netif %p: %u(%c%c:%u, %c%c:%u)\n",
                ppp,
                pUser->userid,
                ppp_netif->name[0],
                ppp_netif->name[1],
                ppp_netif->num,
                netif->name[0],
                netif->name[1],
                netif->num);

    return ERR_SUCCESS;
}

struct netif *get_rawsocket_if(void) {
    return g_rawSocketIf;
}