#include "http.h"
#include "net.h"
#include "util.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>

#define INITIAL_BUF 8192

typedef struct {
    char *data;
    size_t len;
    size_t cap;
} Buf;

static int buf_ensure(Buf *b, size_t extra)
{
    if (b->len + extra + 1 <= b->cap)
        return 0;
    size_t ncap = b->cap ? b->cap * 2 : INITIAL_BUF;
    while (ncap < b->len + extra + 1)
        ncap *= 2;
    char *nd = realloc(b->data, ncap);
    if (!nd)
        return -1;
    b->data = nd;
    b->cap = ncap;
    return 0;
}

static int buf_append(Buf *b, const char *s, size_t n)
{
    if (buf_ensure(b, n) != 0)
        return -1;
    memcpy(b->data + b->len, s, n);
    b->len += n;
    b->data[b->len] = '\0';
    return 0;
}

/* Appends a NUL-terminated C string, sizing itself via strlen() so callers
 * never have to hand-count literal lengths (a previous version did, and got
 * it wrong, silently splicing NUL bytes into outgoing HTTP headers). */
static int buf_append_str(Buf *b, const char *s)
{
    return buf_append(b, s, strlen(s));
}

/* Case-insensitive substring search (portable stand-in for strcasestr). */
static const char *ci_find(const char *hay, size_t haylen, const char *needle)
{
    size_t nlen = strlen(needle);
    if (nlen == 0 || nlen > haylen)
        return NULL;
    for (size_t i = 0; i + nlen <= haylen; i++) {
        size_t j = 0;
        for (; j < nlen; j++) {
            if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
                break;
        }
        if (j == nlen)
            return hay + i;
    }
    return NULL;
}

/* block is the raw response from the status line through the blank line
 * that terminates the headers (block_len does NOT need to include the body). */
static void extract_header(const char *block, size_t block_len, const char *name,
                            char *out, size_t outsize)
{
    out[0] = '\0';
    char pat[64];
    snprintf(pat, sizeof(pat), "\r\n%s:", name);
    const char *p = ci_find(block, block_len, pat);
    if (!p)
        return;

    const char *v = p + 2 + strlen(name) + 1; /* skip \r\n NAME: */
    const char *block_end = block + block_len;
    while (v < block_end && (*v == ' ' || *v == '\t'))
        v++;
    const char *end = ci_find(v, (size_t)(block_end - v), "\r\n");
    if (!end)
        end = block_end;
    size_t vlen = (size_t)(end - v);
    if (vlen >= outsize)
        vlen = outsize - 1;
    memcpy(out, v, vlen);
    out[vlen] = '\0';
}

static long extract_content_length(const char *headers, size_t headers_len)
{
    char v[32];
    extract_header(headers, headers_len, "Content-Length", v, sizeof(v));
    if (v[0] == '\0')
        return -1;
    return atol(v);
}

static int is_chunked(const char *headers, size_t headers_len)
{
    char v[64];
    extract_header(headers, headers_len, "Transfer-Encoding", v, sizeof(v));
    return strcasecmp(v, "chunked") == 0;
}

/* Decodes an HTTP chunked body in-place; returns decoded length, or -1 on malformed input. */
static long decode_chunked(const char *in, size_t inlen, char **out)
{
    Buf b = {0};
    size_t pos = 0;
    while (pos < inlen) {
        const char *line_end = NULL;
        for (size_t i = pos; i + 1 < inlen; i++) {
            if (in[i] == '\r' && in[i + 1] == '\n') {
                line_end = in + i;
                break;
            }
        }
        if (!line_end)
            break;
        long chunk_len = strtol(in + pos, NULL, 16);
        pos = (size_t)(line_end - in) + 2;
        if (chunk_len <= 0)
            break;
        if (pos + (size_t)chunk_len > inlen)
            break;
        if (buf_append(&b, in + pos, (size_t)chunk_len) != 0) {
            free(b.data);
            return -1;
        }
        pos += (size_t)chunk_len;
        if (pos + 2 <= inlen && in[pos] == '\r' && in[pos + 1] == '\n')
            pos += 2;
    }
    *out = b.data;
    return (long)b.len;
}

int http_post_json(const char *host, int port, const char *path,
                    const char *user, const char *pass,
                    const char *session_id_in,
                    const char *body, size_t body_len,
                    HttpResponse *resp, char *err, size_t errlen)
{
    memset(resp, 0, sizeof(*resp));

    Buf req = {0};
    char line[1024];

    int n = snprintf(line, sizeof(line), "POST %s HTTP/1.1\r\n", path);
    buf_append(&req, line, (size_t)n);

    n = snprintf(line, sizeof(line), "Host: %s:%d\r\n", host, port);
    buf_append(&req, line, (size_t)n);

    buf_append_str(&req, "User-Agent: transtui/1.0\r\n");
    buf_append_str(&req, "Content-Type: application/json\r\n");
    buf_append_str(&req, "Accept: application/json\r\n");
    buf_append_str(&req, "Connection: close\r\n");

    n = snprintf(line, sizeof(line), "Content-Length: %zu\r\n", body_len);
    buf_append(&req, line, (size_t)n);

    if (session_id_in && session_id_in[0]) {
        n = snprintf(line, sizeof(line), "X-Transmission-Session-Id: %s\r\n", session_id_in);
        buf_append(&req, line, (size_t)n);
    }

    if (user && user[0]) {
        char cred[256];
        n = snprintf(cred, sizeof(cred), "%s:%s", user, pass ? pass : "");
        char b64[400];
        long enc = base64_encode((unsigned char *)cred, (size_t)n, b64, sizeof(b64));
        if (enc > 0) {
            n = snprintf(line, sizeof(line), "Authorization: Basic %s\r\n", b64);
            buf_append(&req, line, (size_t)n);
        }
    }

    buf_append_str(&req, "\r\n");
    if (body_len)
        buf_append(&req, body, body_len);

    int fd = net_connect(host, port, err, errlen);
    if (fd < 0) {
        free(req.data);
        return -1;
    }

    size_t sent = 0;
    while (sent < req.len) {
        ssize_t w = write(fd, req.data + sent, req.len - sent);
        if (w <= 0) {
            snprintf(err, errlen, "failed to send HTTP request");
            free(req.data);
            close(fd);
            return -1;
        }
        sent += (size_t)w;
    }
    free(req.data);

    Buf resb = {0};
    char chunk[4096];
    long content_length = -1;
    int chunked = 0;
    size_t header_end = 0;
    int have_headers = 0;

    for (;;) {
        ssize_t r = read(fd, chunk, sizeof(chunk));
        if (r < 0) {
            snprintf(err, errlen, "error reading response");
            free(resb.data);
            close(fd);
            return -1;
        }
        if (r == 0)
            break; /* server closed connection */
        if (buf_append(&resb, chunk, (size_t)r) != 0) {
            snprintf(err, errlen, "out of memory");
            free(resb.data);
            close(fd);
            return -1;
        }

        if (!have_headers) {
            char *sep = memmem(resb.data, resb.len, "\r\n\r\n", 4);
            if (sep) {
                have_headers = 1;
                header_end = (size_t)(sep - resb.data) + 4;
                content_length = extract_content_length(resb.data, header_end);
                chunked = is_chunked(resb.data, header_end);
            }
        }
        if (have_headers && !chunked && content_length >= 0) {
            if (resb.len - header_end >= (size_t)content_length)
                break;
        }
    }
    close(fd);

    if (!have_headers) {
        snprintf(err, errlen, "incomplete HTTP response");
        free(resb.data);
        return -1;
    }

    /* Parse status line: "HTTP/1.1 200 OK" */
    int status = 0;
    sscanf(resb.data, "HTTP/%*d.%*d %d", &status);
    resp->status = status;

    extract_header(resb.data, header_end, "X-Transmission-Session-Id", resp->session_id, sizeof(resp->session_id));

    const char *body_start = resb.data + header_end;
    size_t avail = resb.len - header_end;

    if (chunked) {
        char *decoded = NULL;
        long dlen = decode_chunked(body_start, avail, &decoded);
        if (dlen < 0) {
            snprintf(err, errlen, "could not decode chunked response");
            free(resb.data);
            return -1;
        }
        resp->body = decoded;
        resp->body_len = (size_t)dlen;
        free(resb.data);
    } else {
        size_t blen = avail;
        if (content_length >= 0 && (size_t)content_length < blen)
            blen = (size_t)content_length;
        char *b = malloc(blen + 1);
        if (!b) {
            snprintf(err, errlen, "out of memory");
            free(resb.data);
            return -1;
        }
        memcpy(b, body_start, blen);
        b[blen] = '\0';
        resp->body = b;
        resp->body_len = blen;
        free(resb.data);
    }

    return 0;
}

void http_response_free(HttpResponse *resp)
{
    free(resp->body);
    resp->body = NULL;
    resp->body_len = 0;
}
