 /*
  * util.c
  * Copyright (C) Aitor Cuadrado Zubizarreta <aitor@genuen.org>
  * 
  * simple-netaid 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.
  * 
  * simple-netaid 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 this program.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * See the COPYING file.
  */
  
   
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> 
#include <stdbool.h>     
#include <errno.h>
#include <sys/wait.h>  // waitpid
#include <spawn.h>

#include <simple-netaid/sbuf.h>
#include <simple-netaid/interfaces.h>
#include <simple-netaid/helpers.h>
#include <simple-netaid/ipaddr.h>
#include <simple-netaid/ethlink.h>
#include <simple-netaid/iproute.h>

#include "util.h"
#include "config.h"
#include "def.h"
#include "iwlist.h"
   
#define MAX_SIZE 256
#define BUF_SIZE 512
#define MIN_WIFI_QUALITY 15

/**
 * Escape quotation marks and backslashes for a wpa_supplicant SSID.
 * Returns a dynamic string that must be freed with free() after usage.
 */
char *escape_ssid(const char *src)
{
    if (!src) return NULL;

    // Worst case: each char becomes \ + char (ej: \n, \", \\)
    char *dest = malloc(strlen(src) * 2 + 1);
    if (!dest) return NULL;

    char *p = dest;
    while (*src) {
        switch (*src) {
            case '"':
            case '\\':
                *p++ = '\\';
                *p++ = *src;
                break;
            case '\n':
                *p++ = '\\';
                *p++ = 'n';
                break;
            case '\r':
                *p++ = '\\';
                *p++ = 'r';
                break;
            default:
                *p++ = *src;
                break;
        }
        src++;
    }
    *p = '\0';
    return dest;
}

/**
 * Returns 0 if successful, or a negative value if there was an error.
 */
int append_wpa_config(const char *essid, const char *passwd, const char *filename)
{
    int pipefd[2];
    int status;
    pid_t pid;
    ssize_t bytes_read;
    int write_error = 0;
    char *buffer = NULL;
    
    if (pipe(pipefd) == -1)
        return -1;

    pid = fork();
    if (pid == -1) {
        close(pipefd[0]);
        close(pipefd[1]);
        return -2;
    }

    if (pid == 0) {
        /* Child */
        close(pipefd[0]);
        if (dup2(pipefd[1], STDOUT_FILENO) == -1) _exit(EXIT_FAILURE);
        close(pipefd[1]);

        /* execlp is injection-proof: arguments are passed 
         * directly to the kernel; they are not interpreted by /bin/sh */
        execlp("wpa_passphrase", "wpa_passphrase", essid, passwd, (char *)NULL);
        _exit(EXIT_FAILURE); 
    }

    /* Proceso Padre */
    close(pipefd[1]);

    /* Open the destination file (O_APPEND to avoid deleting content) */
    int fd = open(filename, O_WRONLY | O_APPEND | O_CREAT, 0600);
    if (fd == -1) {
        close(pipefd[0]);
        waitpid(pid, NULL, 0);
        return -3;
    }

    buffer = malloc(BUF_SIZE);    
    if (!buffer) {
        close(pipefd[0]);
        waitpid(pid, NULL, 0);
        if (fd != -1) {
            if (fsync(fd) < 0)
                perror("fsync en append_wpa_config");
            close(fd);
        }
        return -6;
    }

    /* Read from the pipe and write to the file */
    while ((bytes_read = read(pipefd[0], buffer, BUF_SIZE)) > 0) {
        if (write(fd, buffer, bytes_read) == -1) {
            write_error = 1;
            break;
        }
    }
    
    free(buffer);

    close(pipefd[0]);

    if (fd != -1) {
        if (fsync(fd) < 0)
            perror("fsync en append_wpa_config");
        /* It is now safe to close */
        close(fd);
    }

    waitpid(pid, &status, 0);

    /* Verify whether the child succeeded */
    if (write_error)
        return -4;

    if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
        /* Succeed */
        return 0;

    return -5;
}

int safe_execute(const char *out_path, char *const argv[])
{
    pid_t pid;
    int status;
    posix_spawn_file_actions_t actions;     
    /* Define a minimum PATH */
    char *env[] = { "PATH=/usr/sbin:/usr/bin:/sbin:/bin", NULL };

    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, out_path, O_WRONLY, 0);

    int spawn_err = posix_spawnp(&pid, argv[0], &actions, NULL, argv, env);
    posix_spawn_file_actions_destroy(&actions);

    if (spawn_err != 0)
        return -1;

    while (waitpid(pid, &status, 0) < 0)
        if (errno != EINTR)
            return -1;

    if (WIFEXITED(status))
        return WEXITSTATUS(status);
    
    /* Ended due to a crash or signal (SIGKILL/SIGSEGV) */
    return -1;
}

bool is_wpa_conf_file(const char *path)
{
    int has_network = 0;
    int has_ssid = 0;
    
    FILE *f = fopen(path, "r");
    if (!f) { 
         log_error("%s: Unable to open %s: %s\n", progname, path, strerror(errno));
         return false; 
    }

    char line[256];
    while (fgets(line, sizeof(line), f)) {
        char *p = line;
        while(*p == ' ' || *p == '\t')
            p++;

        if (strncmp(p, "network={", 9) == 0)
            has_network = 1;
        
        if (strstr(p, "ssid="))
            has_ssid = 1;
        
        if (has_network && has_ssid)
            break;
    }

    fclose(f);
    return (has_network && has_ssid);
}

/**
 * \brief Create the directory to store the pidfile and the logfile.
 */
static void mkdir_piddir(const char *s)
{
    char *piddir, *tmp;

    if (!s || *s == '\0')
        return;

    piddir = strdup(s);
    if (!piddir)
        return;

    tmp = strrchr(piddir, '/');
    if (tmp) {
        *tmp = '\0';
        if (*piddir != '\0') {
            if (mkdir(piddir, 0755) == -1) {
                if (errno != EEXIST)
                    log_error("Couldn't find PID's directory %s: %m", piddir);
            }
        }
    }
    free(piddir);
}
   
/**
 * \brief lock/unlock a file (option=1 locks the file; option=0 unlocks the file).
 * When trying to lock a file that is already locked, the function returns 1.
 */
int lock_file(const char *filename, int option)
{
    int fd;
    struct flock fl;

    /* Open with O_CLOEXEC for security reasons,
     * preventing children from inheriting the lock */
    fd = open(filename, O_RDWR | O_CREAT | O_CLOEXEC, 0640);
    if (fd < 0) {
        log_error("Unable to open lockfile %s: %m", filename);
        return -1;
    }

    memset(&fl, 0, sizeof(fl));
    /* option 1: Lock | option 0: Unlock */
    fl.l_type = (option == 1) ? F_WRLCK : F_UNLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;  /* Block the entire file */

    /* Non-blocking blocking attempt */
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        /* Is it already locked by other instance? */
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return 1;
        }
        close(fd);
        return -1;
    }

    if (option == 1) {
        /* We save it globally so that the lock persists */
        pid_fd = fd;
    } else {
        close(fd);
        if (option == 0)
            unlink(filename);
    }

    return 0;
}

/**
 * \brief Get the PID and check if the process is alive.
 */
pid_t get_pid_t(void)
{
    char txt[64];
    int fd;
    pid_t pid;
    ssize_t l;

    if (!m_config || !m_config->pidfile_path)
        return (pid_t)-1;

    if (access(m_config->pidfile_path, F_OK) == -1)
        return (pid_t) -1;

    fd = open(m_config->pidfile_path, O_RDONLY);
    if (fd < 0)
        return (pid_t)-1;

    l = read(fd, txt, sizeof(txt) - 1);
    close(fd);

    if (l <= 0)
        return (pid_t)-1;
    
    txt[l] = '\0';
    {
        char *end = NULL;
        long parsed = strtol(txt, &end, 10);

        if (!end || end == txt || *end != '\0' || parsed <= 0)
            return (pid_t)-1;
        pid = (pid_t)parsed;
    }

    if (kill(pid, 0) != 0) {
        if (errno == ESRCH) {
            /* The process died, but the file remained there */
            log_warn("Stuck PID file found. Cleaning up...");
            unlink(m_config->pidfile_path);
        }
        return (pid_t)-1;
    }

    return pid;
}

/**
 * \brief Kill the demon and wait for it to disappear.
 */
int wait_on_kill(int sig, int timeout_sec)
{
    pid_t pid = get_pid_t();
    if (pid < 0)
        return -1;

    if (kill(pid, sig) < 0)
        return -1;

    time_t end_time = time(NULL) + timeout_sec;

    while (time(NULL) < end_time) {
        /* If kill(pid, 0) fails with ESRCH, the process terminated */
        if (kill(pid, 0) < 0 && errno == ESRCH) {
            unlink(m_config->pidfile_path);
            if (m_config->lockfile_path) unlink(m_config->lockfile_path);
            return 0;
        }
        
        /* Short wait for 100ms using nanosleep */
        struct timespec ts = {0, 100000000L}; 
        nanosleep(&ts, NULL);
    }

    errno = ETIME;
    return -1;
}

void daemonize()
{
    pid_t pid;
    int fd;

    /* 1st fork */
    pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);

    /* Become a session leader */
    if (setsid() < 0)
        exit(EXIT_FAILURE);

    signal(SIGCHLD, SIG_IGN);

    /* 2nd fork */
    pid = fork();
    if (pid < 0)
        exit(EXIT_FAILURE);
    if (pid > 0)
        exit(EXIT_SUCCESS);

    umask(0);
    if (chdir("/tmp") != 0)
        exit(EXIT_FAILURE);

    /* Close standard descriptors safety */
    for (fd = sysconf(_SC_OPEN_MAX); fd >= 0; fd--)
        close(fd);

    fd = open("/dev/null", O_RDWR);
    if (fd != -1) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > 2)
            close(fd);
    }

    openlog(progname, LOG_PID, LOG_DAEMON);

    if (m_config->pidfile_path) {
        mkdir_piddir(m_config->pidfile_path);
      
        pid_fd = open(m_config->pidfile_path, O_RDWR | O_CREAT, 0640);
        if (pid_fd < 0) {
            log_error("Cannot open pidfile %s: %m", m_config->pidfile_path);
            exit(EXIT_FAILURE);
        }
      
        // F_TLOCK: Fails if already locked, avoiding replicated instance */
        if (lockf(pid_fd, F_TLOCK, 0) < 0) {
            log_error("Daemon already running (lock active)");
            exit(EXIT_FAILURE);
        }

        /* Delete previous content and write new PID */
        if (ftruncate(pid_fd, 0) == -1) {
            log_error("Error al truncar el archivo de PID");
            close(pid_fd);
            exit(EXIT_FAILURE);
        }
        dprintf(pid_fd, "%d\n", getpid());
        /* We do not close pid_fd; the lock remains while the process is active */
    }
}

void get_interfaces_simple(char *wired, size_t w_len, char *wireless, size_t wl_len) 
{
    DIR *dir;
    struct dirent *ent;
    char path[PATH_MAX];

    if (wired) wired[0] = '\0';
    if (wireless) wireless[0] = '\0';

    dir = opendir("/sys/class/net");
    if (!dir) return;

    while ((ent = readdir(dir)) != NULL) {
        // Ignore "." ".." and the loopback "lo"
        if (ent->d_name[0] == '.' || strcmp(ent->d_name, "lo") == 0 ||
                strncmp(ent->d_name, "bond", 4) == 0)
            continue;

        snprintf(path, sizeof(path), "/sys/class/net/%s/wireless", ent->d_name);
        
        if (access(path, F_OK) == 0) {
            if (wireless && wireless[0] == '\0')
                snprintf(wireless, wl_len, "%s", ent->d_name);
        } else {
            if (wired && wired[0] == '\0')
                snprintf(wired, w_len, "%s", ent->d_name);
        }
    }
    closedir(dir);
}

void ipaddr_flush_all_devices()
{
    char wired_if[IFNAMSIZ] = {0};
    char wireless_if[IFNAMSIZ] = {0};

    get_interfaces_simple(wired_if, sizeof(wired_if), wireless_if, sizeof(wireless_if));

    if (wired_if[0] != '\0')
        ipaddr_flush(wired_if);

    if (wireless_if[0] != '\0')
        ipaddr_flush(wireless_if);
}

int wired_connection(const char *ifname)
{ 
    if (!ifname)
        return -1;

    kill_all_processes(ifname, NULL);
    ipaddr_flush(ifname);
    
    char *const ifdown_cmd[] = { "ifdown", "--force", (char *)ifname, NULL };
    safe_execute("/dev/null", ifdown_cmd);

    char *const check_cmd[] = { "ifquery", (char *)ifname, NULL };
    if (safe_execute("/dev/null", check_cmd) != 0) {
        log_error("Interface %s not defined in /etc/network/interfaces", ifname);
        return -1; 
    }
 
    char *const ifup_cmd[] = { "ifup", "-v", (char *)ifname, NULL };
    return safe_execute("/dev/null", ifup_cmd);
}

void wired_teardown(const char *ifname, bool leave_admin_up)
{
    if (!ifname || !ifname[0])
        return;

    kill_all_processes(ifname, NULL);
    ipaddr_flush(ifname);

    char *const ifdown_cmd[] = { "ifdown", "--force", (char *)ifname, NULL };
    safe_execute("/dev/null", ifdown_cmd);

    if (leave_admin_up)
        interface_up(ifname);
    else
        interface_down(ifname);
}

void wired_disconnect(const char *ifname)
{
    /* Physical carrier loss: stay admin UP for the next plug event. */
    wired_teardown(ifname, true);
}

/**
 * Search for an SSID in the JSON buffer and extract its Quality and MAC (Address)..
 * Returns the quality (int) or -1 if it cannot be found.
 */
static int get_info_by_ssid(struct blob_attr *scan_data, const char *target_ssid, char *out_mac)
{
    struct blob_attr *cell;
    int rem1;

    blobmsg_for_each_attr(cell, scan_data, rem1) {
        struct blob_attr *info;
        int rem2;
        char *current_ssid = NULL;
        char *current_mac = NULL;
        int current_quality = -1;

        blobmsg_for_each_attr(info, cell, rem2) {
            const char *name = blobmsg_name(info);

            if (!strcmp(name, "Essid")) {
                current_ssid = blobmsg_get_string(info);
            } else if (!strcmp(name, "Address")) {
                current_mac = blobmsg_get_string(info);
            } else if (!strcmp(name, "Quality")) {
                const char *val_str = blobmsg_get_string(info);
                if (val_str) {
                    char *endptr;
                    long val = strtol(val_str, &endptr, 10);
                    
                    /* verify that at least one digit has been read */
                    if (val_str != endptr)
                        current_quality = (int)val;
                }
            }
        }

        if (current_ssid && !strcmp(current_ssid, target_ssid)) {
            if (current_mac && out_mac) {
                strlcpy(out_mac, current_mac, 18);
            }
            return current_quality;
        }
    }
    return -1;
}

int wireless_connection(const char *ifname)
{
    DIR *dir;
    struct dirent *ent;
    struct blob_buf buffer;
    
    char path[PATH_MAX], best_path[PATH_MAX] = {0};
    char line[MAX_SIZE], best_ssid[128] = {0}, best_mac[18] = {0};
    int best_quality = -1;

    blob_buf_init(&buffer, 0);
    /* Initial scanner */
    if (iwlist(ifname, &buffer) <= 0) {
        blob_buf_free(&buffer);
        return -1;
    }

    dir = opendir(WIFI_DIR);
    if (!dir) {
        log_error("Error: Unable to open %s: %m", WIFI_DIR);
        blob_buf_free(&buffer);
        return -1;
    }

    /* Find out the best known network available */
    while ((ent = readdir(dir))) {
        if (ent->d_name[0] == '.')
            continue;
        if (ent->d_type == DT_DIR)
            continue;

        snprintf(path, sizeof(path), "%s/%s", WIFI_DIR, ent->d_name);
        FILE *fp = fopen(path, "r");
        if (!fp) continue;

        while (fgets(line, sizeof(line), fp)) {
            char *start = strchr(line, '"');
            char *end = start ? strrchr(start + 1, '"') : NULL;

            if (start && end) {
                *end = '\0';
                char *ssid_in_file = start + 1;
                char current_mac[18] = {0};

                /* Binary search in the ubus buffer */
                int quality = get_info_by_ssid(buffer.head, ssid_in_file, current_mac);
                
                if (quality > best_quality) {
                    best_quality = quality;
                    strlcpy(best_ssid, ssid_in_file, sizeof(best_ssid));
                    strlcpy(best_mac, current_mac, sizeof(best_mac));
                    strlcpy(best_path, path, sizeof(best_path)); 
                }
            }
        }
        fclose(fp);
    }
    closedir(dir);
    blob_buf_free(&buffer);

    if (*best_path == '\0') {
        log_warn("No known networks were found in range.");
        return -1;
    }
    
    /* Threashold validation: avoid futile attempts with weak signals */
    if (best_quality < MIN_WIFI_QUALITY) {
        log_warn("Network %s discarded due to low quality: %d/70 (Threshold: %d)", 
               best_ssid, best_quality, MIN_WIFI_QUALITY);
        return -1;
    }

    log_info("Connecting to %s [%s] (Quality: %d/70)...", best_ssid, best_mac, best_quality);

    int retries = 3;
    for (int attempt = 1; attempt <= retries; attempt++) {
        
        /* Surgical cleanup: Kill processes on ifname but ignores eth0 (protected) */
        kill_all_processes(ifname, "eth0"); 

        /* Cleaning up ifupdown2 (Normal + Force) */
        char *const down_args[] = { "ifdown", "-v", (char *)ifname, NULL };
        if (safe_execute("/dev/null", down_args) != 0) {
            char *const force_args[] = { "ifdown", "-v", "--force", (char *)ifname, NULL };
            safe_execute("/dev/null", force_args);
        }

        ipaddr_flush(ifname);
        
        char *const link_up[] = { "ip", "link", "set", (char *)ifname, "up", NULL };
        safe_execute("/dev/null", link_up);

        /* Authentication with forced BSSID */
        char *const wpa_args[] = { 
            "wpa_supplicant", "-B", "-i", (char *)ifname, 
            "-c", best_path, "-b", best_mac, NULL 
        };

        if (safe_execute("/dev/null", wpa_args) == 0) {
            sleep(2);
            
            /* IP configuration via ifupdown2 (post-up dhcpcd) */
            char *const up_args[] = { "ifup", (char *)ifname, NULL };
            if (safe_execute("/dev/null", up_args) == 0) {
                log_info("Success: %s connected on attempt %d", best_ssid, attempt);
                return 0;
            }
        }

        /* Fault diagnosis: We rescan to validate the signal. */
        struct blob_buf retry_buf;
        blob_buf_init(&retry_buf, 0);
        char tmp_mac[18];
        
        int current_q = -1;
        if (iwlist(ifname, &retry_buf) > 0) {
            current_q = get_info_by_ssid(retry_buf.head, best_ssid, tmp_mac);
        }
        blob_buf_free(&retry_buf);

        if (current_q != -1) {
            log_warn("Failed attempt %d. Current quality: %d/70", attempt, current_q);
            if (current_q < MIN_WIFI_QUALITY) {
                log_error("Quality fell below the threshold. Aborting...");
                break; 
            }
            best_quality = current_q; 
        } else {
            log_error("Failed attempt %d. Network %s disappeared!", attempt, best_ssid);
            break; 
        }
        
        sleep(1);
    }

    return -1;
}
