#include "session.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/stat.h>
#include <sys/types.h>

#include <termios.h>

#define LOG_MODULE "session"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "xmalloc.h"

#define JSON_SUFFIX ".json"
#define JSON_SUFFIX_LEN 5
#define SCROLLBACK_SUFFIX ".scrollback.txt"
#define SCROLLBACK_ENC_SUFFIX ".scrollback.enc"
#define SCROLLBACK_MAX_BYTES (1u << 20)  /* 1 MB cap */

static int
state_dir(char *buf, size_t buf_sz)
{
    const char *xdg = getenv("XDG_DATA_HOME");
    int n;
    if (xdg != NULL && xdg[0] == '/')
        n = snprintf(buf, buf_sz, "%s/foot/state", xdg);
    else {
        const char *home = getenv("HOME");
        if (home == NULL)
            return -1;
        n = snprintf(buf, buf_sz, "%s/.local/share/foot/state", home);
    }
    if (n < 0 || (size_t)n >= buf_sz)
        return -1;
    return n;
}

static bool
mkdir_p(const char *path)
{
    char tmp[4096];
    size_t len = strlen(path);
    if (len >= sizeof(tmp))
        return false;
    memcpy(tmp, path, len + 1);

    for (size_t i = 1; i < len; i++) {
        if (tmp[i] == '/') {
            tmp[i] = '\0';
            if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
                LOG_ERRNO("mkdir(%s)", tmp);
                return false;
            }
            tmp[i] = '/';
        }
    }
    if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
        LOG_ERRNO("mkdir(%s)", tmp);
        return false;
    }
    return true;
}

static int
session_path(const char *name, char *buf, size_t buf_sz)
{
    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0)
        return -1;
    int n = snprintf(buf, buf_sz, "%s/%s.json", dir, name);
    if (n < 0 || (size_t)n >= buf_sz)
        return -1;
    return n;
}

bool
session_name_is_valid(const char *name)
{
    if (name == NULL || name[0] == '\0')
        return false;
    for (const char *p = name; *p != '\0'; p++) {
        char c = *p;
        bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
                  (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
        if (!ok)
            return false;
    }
    return true;
}

void
session_state_free(struct session_state *st)
{
    if (st == NULL)
        return;
    free(st->cwd);
    if (st->argv != NULL) {
        for (int i = 0; i < st->argc; i++)
            free(st->argv[i]);
        free(st->argv);
    }
    st->cwd = NULL;
    st->argv = NULL;
    st->argc = 0;
}

#if defined(__linux__)
static char *
read_link(const char *path)
{
    char *buf = NULL;
    size_t cap = 256;
    for (;;) {
        buf = xrealloc(buf, cap);
        ssize_t n = readlink(path, buf, cap - 1);
        if (n < 0) {
            free(buf);
            return NULL;
        }
        if ((size_t)n < cap - 1) {
            buf[n] = '\0';
            return buf;
        }
        cap *= 2;
        if (cap > 1 << 16) {
            free(buf);
            return NULL;
        }
    }
}

static bool
read_cmdline(pid_t pid, int *out_argc, char ***out_argv)
{
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)pid);

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        return false;

    char *buf = NULL;
    size_t len = 0;
    size_t cap = 0;
    char chunk[1024];
    for (;;) {
        ssize_t n = read(fd, chunk, sizeof(chunk));
        if (n < 0) {
            if (errno == EINTR)
                continue;
            close(fd);
            free(buf);
            return false;
        }
        if (n == 0)
            break;
        if (len + (size_t)n + 1 > cap) {
            cap = (cap == 0 ? 1024 : cap * 2);
            while (len + (size_t)n + 1 > cap)
                cap *= 2;
            buf = xrealloc(buf, cap);
        }
        memcpy(buf + len, chunk, n);
        len += n;
    }
    close(fd);

    if (buf == NULL || len == 0) {
        free(buf);
        return false;
    }
    /* /proc/<pid>/cmdline is NUL-separated; ensure trailing NUL */
    if (buf[len - 1] != '\0') {
        buf = xrealloc(buf, len + 1);
        buf[len] = '\0';
        len++;
    }

    /* Count args */
    int argc = 0;
    for (size_t i = 0; i < len; i++)
        if (buf[i] == '\0' && (i == 0 || buf[i - 1] != '\0'))
            argc++;

    if (argc <= 0) {
        free(buf);
        return false;
    }

    char **argv = xmalloc(sizeof(char *) * (argc + 1));
    int idx = 0;
    size_t start = 0;
    for (size_t i = 0; i < len && idx < argc; i++) {
        if (buf[i] == '\0') {
            argv[idx++] = xstrdup(buf + start);
            start = i + 1;
        }
    }
    argv[argc] = NULL;
    free(buf);

    *out_argc = argc;
    *out_argv = argv;
    return true;
}
#endif

bool
session_capture(int ptmx, const char *fallback_cwd, struct session_state *out)
{
    memset(out, 0, sizeof(*out));

#if defined(__linux__)
    pid_t fg = tcgetpgrp(ptmx);
    if (fg > 0) {
        char path[64];
        snprintf(path, sizeof(path), "/proc/%d/cwd", (int)fg);
        char *cwd = read_link(path);
        if (cwd != NULL)
            out->cwd = cwd;

        int argc = 0;
        char **argv = NULL;
        if (read_cmdline(fg, &argc, &argv)) {
            /* If the foreground process is a shell (login or otherwise), don't
             * try to resurrect it via argv — let the user's normal shell
             * launch take over. We only restore non-shell processes. */
            const char *base = argv[0];
            const char *slash = strrchr(base, '/');
            if (slash != NULL)
                base = slash + 1;
            const char *shells[] = {
                "bash", "zsh", "fish", "sh", "dash", "ksh", "tcsh", "csh",
                "-bash", "-zsh", "-fish", "-sh", "-dash"
            };
            bool is_shell = false;
            for (size_t i = 0; i < sizeof(shells) / sizeof(shells[0]); i++) {
                if (strcmp(base, shells[i]) == 0) {
                    is_shell = true;
                    break;
                }
            }
            if (is_shell) {
                for (int i = 0; i < argc; i++)
                    free(argv[i]);
                free(argv);
            } else {
                out->argc = argc;
                out->argv = argv;
            }
        }
    }
#endif

    if (out->cwd == NULL && fallback_cwd != NULL)
        out->cwd = xstrdup(fallback_cwd);

    return out->cwd != NULL;
}

/* ------------- JSON encoding/decoding (minimal, hand-rolled) ------------- */

static void
json_escape(FILE *fp, const char *s)
{
    fputc('"', fp);
    for (const char *p = s; *p != '\0'; p++) {
        unsigned char c = (unsigned char)*p;
        switch (c) {
        case '"': fputs("\\\"", fp); break;
        case '\\': fputs("\\\\", fp); break;
        case '\b': fputs("\\b", fp); break;
        case '\f': fputs("\\f", fp); break;
        case '\n': fputs("\\n", fp); break;
        case '\r': fputs("\\r", fp); break;
        case '\t': fputs("\\t", fp); break;
        default:
            if (c < 0x20)
                fprintf(fp, "\\u%04x", c);
            else
                fputc(c, fp);
        }
    }
    fputc('"', fp);
}

bool
session_save(const char *name, const struct session_state *st)
{
    if (!session_name_is_valid(name)) {
        LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
        return false;
    }
    if (st == NULL || st->cwd == NULL) {
        LOG_ERR("session_save: missing cwd");
        return false;
    }

    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0) {
        LOG_ERR("could not resolve state directory");
        return false;
    }
    if (!mkdir_p(dir))
        return false;

    char path[1280];
    if (session_path(name, path, sizeof(path)) < 0)
        return false;

    char tmp_path[1408];
    snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);

    FILE *fp = fopen(tmp_path, "we");
    if (fp == NULL) {
        LOG_ERRNO("fopen(%s)", tmp_path);
        return false;
    }

    fputs("{\n  \"cwd\": ", fp);
    json_escape(fp, st->cwd);
    if (st->argc > 0 && st->argv != NULL) {
        fputs(",\n  \"argv\": [", fp);
        for (int i = 0; i < st->argc; i++) {
            if (i > 0)
                fputs(", ", fp);
            json_escape(fp, st->argv[i]);
        }
        fputc(']', fp);
    }
    fputs("\n}\n", fp);

    if (fflush(fp) != 0 || fsync(fileno(fp)) != 0) {
        LOG_ERRNO("fsync(%s)", tmp_path);
        fclose(fp);
        unlink(tmp_path);
        return false;
    }
    fclose(fp);

    if (rename(tmp_path, path) < 0) {
        LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
        unlink(tmp_path);
        return false;
    }

    LOG_INFO("session saved: %s", path);
    return true;
}

/* ------------- JSON parsing (just enough for our schema) ------------- */

struct parser {
    const char *p;
    const char *end;
};

static void
skip_ws(struct parser *pa)
{
    while (pa->p < pa->end) {
        char c = *pa->p;
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
            pa->p++;
        else
            break;
    }
}

static bool
parse_string(struct parser *pa, char **out)
{
    skip_ws(pa);
    if (pa->p >= pa->end || *pa->p != '"')
        return false;
    pa->p++;

    size_t cap = 64;
    size_t len = 0;
    char *buf = xmalloc(cap);

    while (pa->p < pa->end && *pa->p != '"') {
        char c = *pa->p++;
        char decoded;
        if (c == '\\') {
            if (pa->p >= pa->end) { free(buf); return false; }
            char esc = *pa->p++;
            switch (esc) {
            case '"': decoded = '"'; break;
            case '\\': decoded = '\\'; break;
            case '/': decoded = '/'; break;
            case 'b': decoded = '\b'; break;
            case 'f': decoded = '\f'; break;
            case 'n': decoded = '\n'; break;
            case 'r': decoded = '\r'; break;
            case 't': decoded = '\t'; break;
            case 'u': {
                if (pa->end - pa->p < 4) { free(buf); return false; }
                unsigned val = 0;
                for (int i = 0; i < 4; i++) {
                    char h = pa->p[i];
                    val <<= 4;
                    if (h >= '0' && h <= '9') val |= h - '0';
                    else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
                    else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
                    else { free(buf); return false; }
                }
                pa->p += 4;
                if (val < 0x80) {
                    decoded = (char)val;
                } else {
                    /* UTF-8 encode codepoint (no surrogate-pair handling) */
                    char enc[4];
                    int n;
                    if (val < 0x800) {
                        enc[0] = (char)(0xc0 | (val >> 6));
                        enc[1] = (char)(0x80 | (val & 0x3f));
                        n = 2;
                    } else {
                        enc[0] = (char)(0xe0 | (val >> 12));
                        enc[1] = (char)(0x80 | ((val >> 6) & 0x3f));
                        enc[2] = (char)(0x80 | (val & 0x3f));
                        n = 3;
                    }
                    if (len + (size_t)n + 1 > cap) {
                        while (len + (size_t)n + 1 > cap) cap *= 2;
                        buf = xrealloc(buf, cap);
                    }
                    memcpy(buf + len, enc, n);
                    len += n;
                    continue;
                }
                break;
            }
            default: free(buf); return false;
            }
        } else {
            decoded = c;
        }
        if (len + 2 > cap) {
            cap *= 2;
            buf = xrealloc(buf, cap);
        }
        buf[len++] = decoded;
    }
    if (pa->p >= pa->end) { free(buf); return false; }
    pa->p++;  /* consume closing quote */
    buf[len] = '\0';
    *out = buf;
    return true;
}

static bool
expect_char(struct parser *pa, char c)
{
    skip_ws(pa);
    if (pa->p >= pa->end || *pa->p != c)
        return false;
    pa->p++;
    return true;
}

bool
session_load(const char *name, struct session_state *out)
{
    memset(out, 0, sizeof(*out));

    if (!session_name_is_valid(name)) {
        LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
        return false;
    }

    char path[1280];
    if (session_path(name, path, sizeof(path)) < 0)
        return false;

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) {
        LOG_ERRNO("open(%s)", path);
        return false;
    }
    struct stat sb;
    if (fstat(fd, &sb) < 0 || sb.st_size <= 0 || sb.st_size > (off_t)(1 << 20)) {
        LOG_ERR("session file %s has unexpected size", path);
        close(fd);
        return false;
    }
    char *data = xmalloc(sb.st_size + 1);
    ssize_t got = 0;
    while (got < sb.st_size) {
        ssize_t n = read(fd, data + got, sb.st_size - got);
        if (n < 0) {
            if (errno == EINTR) continue;
            free(data);
            close(fd);
            return false;
        }
        if (n == 0) break;
        got += n;
    }
    close(fd);
    data[got] = '\0';

    struct parser pa = { .p = data, .end = data + got };
    bool ok = false;

    if (!expect_char(&pa, '{')) goto done;

    for (;;) {
        skip_ws(&pa);
        if (pa.p < pa.end && *pa.p == '}') { pa.p++; break; }

        char *key = NULL;
        if (!parse_string(&pa, &key)) goto done;
        if (!expect_char(&pa, ':')) { free(key); goto done; }

        if (strcmp(key, "cwd") == 0) {
            free(key);
            if (!parse_string(&pa, &out->cwd)) goto done;
        } else if (strcmp(key, "argv") == 0) {
            free(key);
            if (!expect_char(&pa, '[')) goto done;
            int cap = 4;
            out->argv = xmalloc(sizeof(char *) * cap);
            out->argc = 0;
            for (;;) {
                skip_ws(&pa);
                if (pa.p < pa.end && *pa.p == ']') { pa.p++; break; }
                if (out->argc + 1 >= cap) {
                    cap *= 2;
                    out->argv = xrealloc(out->argv, sizeof(char *) * cap);
                }
                if (!parse_string(&pa, &out->argv[out->argc])) goto done;
                out->argc++;
                skip_ws(&pa);
                if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
            }
            out->argv[out->argc] = NULL;
        } else {
            free(key);
            /* unknown key: skip a string value */
            char *junk = NULL;
            if (!parse_string(&pa, &junk)) goto done;
            free(junk);
        }

        skip_ws(&pa);
        if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
    }

    ok = out->cwd != NULL;

done:
    free(data);
    if (!ok)
        session_state_free(out);
    return ok;
}

/* ------------- listing ------------- */

static int
cmp_names(const void *a, const void *b)
{
    return strcmp(*(const char *const *)a, *(const char *const *)b);
}

char **
session_list(size_t *out_count)
{
    *out_count = 0;

    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0)
        return NULL;

    DIR *d = opendir(dir);
    if (d == NULL)
        return NULL;

    size_t cap = 8;
    size_t count = 0;
    char **names = xmalloc(sizeof(char *) * cap);

    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        const char *n = de->d_name;
        size_t nlen = strlen(n);
        if (nlen <= JSON_SUFFIX_LEN)
            continue;
        if (strcmp(n + nlen - JSON_SUFFIX_LEN, JSON_SUFFIX) != 0)
            continue;
        char *stripped = xmalloc(nlen - JSON_SUFFIX_LEN + 1);
        memcpy(stripped, n, nlen - JSON_SUFFIX_LEN);
        stripped[nlen - JSON_SUFFIX_LEN] = '\0';
        if (!session_name_is_valid(stripped)) {
            free(stripped);
            continue;
        }
        if (count >= cap) {
            cap *= 2;
            names = xrealloc(names, sizeof(char *) * cap);
        }
        names[count++] = stripped;
    }
    closedir(d);

    qsort(names, count, sizeof(char *), cmp_names);

    *out_count = count;
    return names;
}

bool
session_exists(const char *name)
{
    if (!session_name_is_valid(name))
        return false;
    char path[1280];
    if (session_path(name, path, sizeof(path)) < 0)
        return false;
    return access(path, F_OK) == 0;
}

bool
session_delete(const char *name)
{
    if (!session_name_is_valid(name))
        return false;
    char path[1280];
    if (session_path(name, path, sizeof(path)) < 0)
        return false;
    if (unlink(path) < 0) {
        LOG_ERRNO("unlink(%s)", path);
        return false;
    }
    session_delete_scrollback(name);
    session_delete_enc_scrollback(name);
    LOG_INFO("session deleted: %s", path);
    return true;
}

static int
scrollback_path(const char *name, char *buf, size_t buf_sz)
{
    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0)
        return -1;
    int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_SUFFIX);
    if (n < 0 || (size_t)n >= buf_sz)
        return -1;
    return n;
}

bool
session_load_scrollback(const char *name, char **out_text, size_t *out_len)
{
    *out_text = NULL;
    *out_len = 0;
    if (!session_name_is_valid(name))
        return false;

    char path[1280];
    if (scrollback_path(name, path, sizeof(path)) < 0)
        return false;

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        return false;

    struct stat sb;
    if (fstat(fd, &sb) < 0 || sb.st_size < 0 ||
        (size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4 /* generous on load */) {
        close(fd);
        return false;
    }
    if (sb.st_size == 0) {
        close(fd);
        return false;
    }

    char *data = xmalloc(sb.st_size + 1);
    ssize_t got = 0;
    while (got < sb.st_size) {
        ssize_t n = read(fd, data + got, sb.st_size - got);
        if (n < 0) {
            if (errno == EINTR) continue;
            free(data);
            close(fd);
            return false;
        }
        if (n == 0) break;
        got += n;
    }
    close(fd);
    data[got] = '\0';

    *out_text = data;
    *out_len = (size_t)got;
    return true;
}

void
session_delete_scrollback(const char *name)
{
    if (!session_name_is_valid(name))
        return;
    char path[1280];
    if (scrollback_path(name, path, sizeof(path)) < 0)
        return;
    unlink(path);
}

static int
enc_path(const char *name, char *buf, size_t buf_sz)
{
    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0)
        return -1;
    int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_ENC_SUFFIX);
    if (n < 0 || (size_t)n >= buf_sz)
        return -1;
    return n;
}

bool
session_has_enc_scrollback(const char *name)
{
    if (!session_name_is_valid(name))
        return false;
    char path[1280];
    if (enc_path(name, path, sizeof(path)) < 0)
        return false;
    return access(path, F_OK) == 0;
}

void
session_delete_enc_scrollback(const char *name)
{
    if (!session_name_is_valid(name))
        return;
    char path[1280];
    if (enc_path(name, path, sizeof(path)) < 0)
        return;
    unlink(path);
}

bool
session_write_enc_blob(const char *name, const unsigned char *blob, size_t len)
{
    if (!session_name_is_valid(name))
        return false;
    char dir[1024];
    if (state_dir(dir, sizeof(dir)) < 0)
        return false;
    if (!mkdir_p(dir))
        return false;
    char path[1280];
    if (enc_path(name, path, sizeof(path)) < 0)
        return false;
    char tmp_path[1408];
    snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);

    /* 0600 — encrypted but still reduces accidental exposure. */
    int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
    if (fd < 0) {
        LOG_ERRNO("open(%s)", tmp_path);
        return false;
    }
    size_t written = 0;
    while (written < len) {
        ssize_t n = write(fd, blob + written, len - written);
        if (n < 0) {
            if (errno == EINTR) continue;
            LOG_ERRNO("write(%s)", tmp_path);
            close(fd);
            unlink(tmp_path);
            return false;
        }
        written += n;
    }
    if (fsync(fd) < 0) {
        LOG_ERRNO("fsync(%s)", tmp_path);
        close(fd);
        unlink(tmp_path);
        return false;
    }
    close(fd);
    if (rename(tmp_path, path) < 0) {
        LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
        unlink(tmp_path);
        return false;
    }
    return true;
}

bool
session_read_enc_blob(const char *name, unsigned char **out, size_t *out_len)
{
    *out = NULL;
    *out_len = 0;
    if (!session_name_is_valid(name))
        return false;
    char path[1280];
    if (enc_path(name, path, sizeof(path)) < 0)
        return false;
    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0)
        return false;
    struct stat sb;
    if (fstat(fd, &sb) < 0 || sb.st_size <= 0 ||
        (size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4) {
        close(fd);
        return false;
    }
    unsigned char *buf = xmalloc(sb.st_size);
    ssize_t got = 0;
    while (got < sb.st_size) {
        ssize_t n = read(fd, buf + got, sb.st_size - got);
        if (n < 0) {
            if (errno == EINTR) continue;
            free(buf);
            close(fd);
            return false;
        }
        if (n == 0) break;
        got += n;
    }
    close(fd);
    if (got != sb.st_size) {
        free(buf);
        return false;
    }
    *out = buf;
    *out_len = (size_t)got;
    return true;
}

void
session_free_names(char **names, size_t count)
{
    if (names == NULL)
        return;
    for (size_t i = 0; i < count; i++)
        free(names[i]);
    free(names);
}
